跳转到内容

User:SuperGrey/gadgets/voter/main.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/voter]]
// <nowiki>
(() => {
  // src/state.ts
  var State = class {
    constructor() {
      // 簡繁轉換
      this.convByVar = function(langDict) {
        if (langDict && langDict.hant) {
          return langDict.hant;
        }
        return "繁簡轉換未初始化,且 langDict 無效!";
      };
      // 用戶名
      this.userName = mw.config.get("wgUserName") || "Example";
      // 頁面名稱
      this.pageName = mw.config.get("wgPageName");
      /**
       * 版本號
       */
      this.version = "4.2.0";
      // MediaWiki API 實例
      this._api = null;
    }
    initHanAssist() {
      return mw.loader.using("ext.gadget.HanAssist").then((require2) => {
        const { convByVar } = require2("ext.gadget.HanAssist");
        if (typeof convByVar === "function") {
          this.convByVar = convByVar;
        }
      });
    }
    getApi() {
      if (!this._api) {
        this._api = new mw.Api({ "User-Agent": `ReviewTool/${this.version}` });
      }
      return this._api;
    }
  };
  var state = new State();
  var state_default = state;

  // src/api.ts
  async function getXToolsInfo(pageName) {
    function safeToLocaleString(num) {
      if (typeof num === "number" && !isNaN(num)) {
        return num.toLocaleString();
      }
      return "0";
    }
    try {
      const pageInfo = await $.get("https://xtools.wmcloud.org/api/page/pageinfo/" + mw.config.get("wgServerName") + "/" + pageName.replace(/["?%&+\\]/g, escape));
      const project = pageInfo.project;
      const pageEnc = encodeURIComponent(pageInfo.page);
      const pageUrl = `https://${project}/wiki/${pageInfo.page}`;
      const pageinfoUrl = `https://xtools.wmcloud.org/pageinfo/${project}/${pageEnc}`;
      const permaLinkUrl = `https://${project}/wiki/Special:PermaLink%2F${pageInfo.created_rev_id}`;
      const diffUrl = `https://${project}/wiki/Special:Diff%2F${pageInfo.modified_rev_id}`;
      const pageviewsUrl = `https://pageviews.wmcloud.org/?project=${project}&pages=${pageEnc}&range=latest-${pageInfo.pageviews_offset}`;
      const creatorLink = `https://${project}/wiki/User:${pageInfo.creator}`;
      const creatorContribsUrl = `https://${project}/wiki/Special:Contributions/${pageInfo.creator}`;
      const createdDate = new Date(pageInfo.created_at).toISOString().split("T")[0];
      const revisionsText = safeToLocaleString(pageInfo.revisions);
      const editorsText = safeToLocaleString(pageInfo.editors);
      const watchersText = safeToLocaleString(pageInfo.watchers);
      const pageviewsText = safeToLocaleString(pageInfo.pageviews);
      const days = Math.round(pageInfo.secs_since_last_edit / 86400);
      let creatorText = "";
      if (pageInfo.creator_editcount) {
        creatorText = `<bdi><a href="${creatorLink}" target="_blank">${pageInfo.creator}</a></bdi> (<a href="${creatorContribsUrl}" target="_blank">${safeToLocaleString(pageInfo.creator_editcount)}</a>)`;
      } else {
        creatorText = `<bdi><a href="${creatorContribsUrl}" target="_blank">${pageInfo.creator}</a></bdi>`;
      }
      let pageCreationText = `「<a target="_blank" title="評級: ${pageInfo.assessment.value}" href="${pageinfoUrl}"><img src="${pageInfo.assessment.badge}" style="height:16px !important; vertical-align:-4px; margin-right:3px"/></a><bdi><a target="_blank" href="${pageUrl}">${pageInfo.page}</a></bdi>」由 ${creatorText} 於 <bdi><a target='_blank' href='${permaLinkUrl}'>${createdDate}</a></bdi> 建立,共 ${revisionsText} 個修訂,最後修訂於 <a href="${diffUrl}">${days} 天</a>前。`;
      let pageEditorsText = `共 ${editorsText} 編輯者` + (watchersText !== "0" ? `、${watchersText} 監視者` : "") + `,最近 ${pageInfo.pageviews_offset} 天共 <a target="_blank" href="${pageviewsUrl}">${pageviewsText} 瀏覽數</a>。`;
      return `<span style="line-height:20px">${pageCreationText}${pageEditorsText}<a target="_blank" href="${pageinfoUrl}">檢視完整頁面統計</a>。</span>`.trim();
    } catch (error) {
      console.error("[Voter] Error fetching XTools data:", error);
      return '<span style="color: red; font-weight: bold;">無法獲取 XTools 頁面資訊。</span>';
    }
  }
  async function voteAPI(tracePage, destPage, sectionID, text, summary) {
    var _a;
    let votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === sectionID)) == null ? void 0 : _a.label) || `section ${sectionID}`;
    mw.notify(`正在為「${votedPageName}」投出一票⋯⋯`);
    let res = await state_default.getApi().get({
      action: "query",
      titles: destPage,
      prop: "revisions|info",
      rvslots: "*",
      rvprop: "content",
      rvsection: sectionID,
      indexpageids: 1
    });
    let page = res.query.pages[res.query.pageids[0]];
    let sectionText = page.revisions[0].slots.main["*"];
    if (sectionText === void 0 || sectionText === "") {
      console.log(`[Voter] 無法取得「${votedPageName}」的投票區段內容。區段ID:${sectionID}。API 回傳:`, res);
      mw.notify(`無法取得「${votedPageName}」的投票區段內容,請刷新後重試。`);
      return true;
    }
    if (!textMatchTitleVariants(sectionText, votedPageName)) {
      console.log(`[Voter] 在「${votedPageName}」的投票區段中找不到該條目。區段文本:`, sectionText);
      mw.notify(`在該章節找不到名為「${votedPageName}」的提名,請刷新後重試。`);
      return true;
    }
    let mod = {
      action: "edit",
      title: destPage,
      section: sectionID,
      summary,
      token: mw.user.tokens.get("csrfToken")
    };
    let innerHeadings;
    if (tracePage === "Wikipedia:新条目推荐/候选") {
      innerHeadings = sectionText.match(/=====.+?=====/g);
    } else {
      innerHeadings = sectionText.match(/===.+?===/g);
    }
    if (innerHeadings) {
      mod.section += 1;
      mod.prependtext = text + "\n";
    } else {
      mod.appendtext = "\n" + text;
    }
    await state_default.getApi().post(mod);
    mw.notify(`「${votedPageName}」已完成投票。`);
    return false;
  }

  // src/dialog.ts
  var _mountedApp = null;
  function loadCodexAndVue() {
    return mw.loader.using("@wikimedia/codex").then((require2) => ({
      Vue: require2("vue"),
      Codex: require2("@wikimedia/codex")
    }));
  }
  function createDialogMountIfNeeded() {
    if (!document.getElementById("review-tool-dialog-mount")) {
      const mountPoint = document.createElement("div");
      mountPoint.id = "review-tool-dialog-mount";
      document.body.appendChild(mountPoint);
    }
    return document.getElementById("review-tool-dialog-mount");
  }
  function mountApp(app) {
    createDialogMountIfNeeded();
    _mountedApp = app.mount("#review-tool-dialog-mount");
    return _mountedApp;
  }
  function getMountedApp() {
    return _mountedApp;
  }
  function removeDialogMount() {
    const mountPoint = document.getElementById("review-tool-dialog-mount");
    if (mountPoint) mountPoint.remove();
    _mountedApp = null;
  }
  function registerCodexComponents(app, Codex) {
    if (!app || !Codex) return;
    try {
      app.component("cdx-dialog", Codex.CdxDialog).component("cdx-text-input", Codex.CdxTextInput).component("cdx-text-area", Codex.CdxTextArea).component("cdx-checkbox", Codex.CdxCheckbox).component("cdx-select", Codex.CdxSelect).component("cdx-button", Codex.CdxButton).component("cdx-button-group", Codex.CdxButtonGroup);
    } catch (e) {
    }
  }

  // src/vote_dialog.ts
  function createVoteDialog(sectionID) {
    loadCodexAndVue().then(({ Vue, Codex }) => {
      const app = Vue.createMwApp({
        i18n: {
          dialogTitle: state_default.convByVar({
            hant: `投票助手 (Voter v${state_default.version})`,
            hans: `投票助手 (Voter v${state_default.version})`
          }),
          submitting: state_default.convByVar({ hant: "儲存中…", hans: "保存中…" }),
          submit: state_default.convByVar({ hant: "儲存投票", hans: "保存投票" }),
          cancel: state_default.convByVar({ hant: "取消", hans: "取消" }),
          next: state_default.convByVar({ hant: "下一步", hans: "下一步" }),
          previous: state_default.convByVar({ hant: "上一步", hans: "上一步" }),
          // Step 0: Entry Selection
          selectEntries: state_default.convByVar({ hant: "投票條目", hans: "投票条目" }),
          selectEntriesPlaceholder: state_default.convByVar({ hant: "選擇要投票的條目", hans: "选择要投票的条目" }),
          // Step 1: Template Selection
          selectTemplates: state_default.convByVar({ hant: "投票模板", hans: "投票模板" }),
          selectTemplatesPlaceholder: state_default.convByVar({ hant: "選擇投票模板", hans: "选择投票模板" }),
          voteReason: state_default.convByVar({ hant: "投票理由(可不填;無須簽名)", hans: "投票理由(可不填;无须签名)" }),
          voteReasonPlaceholder: state_default.convByVar({ hant: "輸入投票理由…", hans: "输入投票理由…" }),
          useBulleted: state_default.convByVar({ hant: "使用 * 縮排", hans: "使用 * 缩进" }),
          // Step 2: Preview
          previewHeading: state_default.convByVar({ hant: "預覽投票內容", hans: "预览投票内容" }),
          previewInfo: state_default.convByVar({ hant: "以下是將要提交的投票內容:", hans: "以下是将要提交的投票内容:" }),
          votingFor: state_default.convByVar({ hant: "投票條目:", hans: "投票条目:" }),
          voteContent: state_default.convByVar({ hant: "投票內容:", hans: "投票内容:" }),
          // Validation
          noEntriesSelected: state_default.convByVar({ hant: "請選擇至少一個投票條目。", hans: "请选择至少一个投票条目。" }),
          noTemplatesSelected: state_default.convByVar({ hant: "請選擇至少一個投票模板。", hans: "请选择至少一个投票模板。" })
        },
        data() {
          return {
            open: true,
            isSubmitting: false,
            currentStep: 0,
            totalSteps: 3,
            // Step 0: Entry selection
            selectedEntries: [sectionID],
            entryInfoHtml: "",
            isLoadingInfo: false,
            // Step 1: Template & reason
            selectedTemplates: state_default.validVoteTemplates.length > 0 ? [state_default.validVoteTemplates[0].data] : [],
            voteMessage: "",
            useBulleted: true
          };
        },
        computed: {
          entryOptions() {
            return state_default.sectionTitles.map((item) => ({ value: item.data, label: item.label }));
          },
          validTemplateOptions() {
            return (state_default.validVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
          },
          invalidTemplateOptions() {
            return (state_default.invalidVoteTemplates || []).map((item) => ({ value: item.data, label: item.label }));
          },
          allTemplateOptions() {
            return [...this.validTemplateOptions, ...this.invalidTemplateOptions];
          },
          selectedEntryNames() {
            return this.selectedEntries.map((id) => {
              const entry = state_default.sectionTitles.find((x) => x.data === id);
              return entry ? entry.label : `Section ${id}`;
            });
          },
          previewVoteText() {
            let VTReason = this.selectedTemplates.map((str) => `{{${str}}}`).join(";");
            const message = (this.voteMessage || "").trim();
            VTReason += message ? ":" + message : "。";
            VTReason += "--~~~~";
            return VTReason;
          },
          primaryAction() {
            if (this.currentStep < this.totalSteps - 1) {
              return { label: this.$options.i18n.next, actionType: "primary", disabled: false };
            }
            return {
              label: this.isSubmitting ? this.$options.i18n.submitting : this.$options.i18n.submit,
              actionType: "progressive",
              disabled: this.isSubmitting
            };
          },
          defaultAction() {
            if (this.currentStep > 0) {
              return { label: this.$options.i18n.previous };
            }
            return { label: this.$options.i18n.cancel };
          }
        },
        watch: {
          selectedEntries: {
            handler: "loadEntryInfo",
            immediate: true
          }
        },
        methods: {
          getStepClass(step) {
            return { "voter-multistep-dialog__stepper__step--active": step <= this.currentStep };
          },
          async loadEntryInfo() {
            if (!this.selectedEntries.length) {
              this.entryInfoHtml = "";
              return;
            }
            this.isLoadingInfo = true;
            const infoPromises = this.selectedEntries.map(async (id) => {
              var _a;
              const entryName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || "";
              if (!entryName) return "";
              return await getXToolsInfo(entryName);
            });
            const results = await Promise.all(infoPromises);
            this.entryInfoHtml = results.filter(Boolean).map((html) => `<div style="margin-top:0.5em">${html}</div>`).join("");
            this.isLoadingInfo = false;
          },
          validateStep0() {
            if (!this.selectedEntries.length) {
              mw.notify(this.$options.i18n.noEntriesSelected, { type: "error", title: "[Voter]" });
              return false;
            }
            return true;
          },
          validateStep1() {
            if (!this.selectedTemplates.length) {
              mw.notify(this.$options.i18n.noTemplatesSelected, { type: "error", title: "[Voter]" });
              return false;
            }
            return true;
          },
          onPrimaryAction() {
            if (this.currentStep === 0 && !this.validateStep0()) {
              return;
            }
            if (this.currentStep === 1 && !this.validateStep1()) {
              return;
            }
            if (this.currentStep < this.totalSteps - 1) {
              this.currentStep++;
              return;
            }
            this.submitVote();
          },
          onDefaultAction() {
            if (this.currentStep > 0) {
              this.currentStep--;
              return;
            }
            this.closeDialog();
          },
          onUpdateOpen(newValue) {
            if (!newValue) {
              this.closeDialog();
            }
          },
          closeDialog() {
            this.open = false;
            setTimeout(() => {
              removeDialogMount();
            }, 300);
          },
          async submitVote() {
            this.isSubmitting = true;
            try {
              const hasConflict = await vote(
                this.selectedEntries,
                this.selectedTemplates,
                this.voteMessage,
                this.useBulleted
              );
              if (hasConflict) {
                this.isSubmitting = false;
                return;
              }
              mw.notify(state_default.convByVar({ hant: "投票已成功提交。", hans: "投票已成功提交。" }), { tag: "voter" });
              this.isSubmitting = false;
              this.open = false;
              setTimeout(() => {
                removeDialogMount();
              }, 300);
            } catch (error) {
              console.error("[Voter] submitVote failed:", error);
              const msg = state_default.convByVar({ hant: "投票提交失敗,請稍後再試。", hans: "投票提交失败,请稍后再试。" });
              mw.notify(msg, { type: "error", title: "[Voter]" });
              this.isSubmitting = false;
            }
          }
        },
        template: `
                <cdx-dialog
                    v-model:open="open"
                    :title="$options.i18n.dialogTitle"
                    :use-close-button="true"
                    :primary-action="primaryAction"
                    :default-action="defaultAction"
                    @primary="onPrimaryAction"
                    @default="onDefaultAction"
                    @update:open="onUpdateOpen"
                    class="voter-dialog voter-multistep-dialog"
                >
                    <template #header>
                        <div class="voter-multistep-dialog__header-top">
                            <h2>{{ $options.i18n.dialogTitle }}</h2>
                        </div>

                        <div class="voter-multistep-dialog__stepper">
                            <div class="voter-multistep-dialog__stepper__label">{{ (currentStep + 1) + ' / ' + totalSteps }}</div>
                            <div class="voter-multistep-dialog__stepper__steps" aria-hidden="true">
                                <span
                                    v-for="step of [0, 1, 2]"
                                    :key="step"
                                    class="voter-multistep-dialog__stepper__step"
                                    :class="getStepClass(step)"
                                ></span>
                            </div>
                        </div>
                    </template>

                    <!-- Step 0: Entry Selection -->
                    <div v-if="currentStep === 0" class="voter-form-section">
                        <label class="voter-form-label">{{ $options.i18n.selectEntries }}</label>
                        <div class="voter-checkbox-grid">
                            <cdx-checkbox
                                v-for="option in entryOptions"
                                :key="option.value"
                                v-model="selectedEntries"
                                :input-value="option.value"
                            >
                                {{ option.label }}
                            </cdx-checkbox>
                        </div>

                        <div
                            v-if="entryInfoHtml"
                            class="voter-entry-info"
                            v-html="entryInfoHtml"
                        ></div>
                        <div v-else-if="isLoadingInfo" class="voter-entry-info voter-entry-info--loading">
                            載入中...
                        </div>
                    </div>

                    <!-- Step 1: Template Selection & Reason -->
                    <div v-else-if="currentStep === 1" class="voter-form-section">
                        <label class="voter-form-label">{{ $options.i18n.selectTemplates }}</label>
                        <div class="voter-checkbox-grid">
                            <cdx-checkbox
                                v-for="option in allTemplateOptions"
                                :key="option.value"
                                v-model="selectedTemplates"
                                :input-value="option.value"
                            >
                                {{ option.label }}
                            </cdx-checkbox>
                        </div>

                        <div class="voter-form-section">
                            <label class="voter-form-label">{{ $options.i18n.voteReason }}</label>
                            <cdx-text-area
                                v-model="voteMessage"
                                :placeholder="$options.i18n.voteReasonPlaceholder"
                                rows="3"
                            ></cdx-text-area>
                        </div>

                        <div class="voter-form-section" style="padding-top: 0;">
                            <cdx-checkbox v-model="useBulleted">
                                {{ $options.i18n.useBulleted }}
                            </cdx-checkbox>
                        </div>
                    </div>

                    <!-- Step 2: Preview -->
                    <div v-else-if="currentStep === 2" class="voter-preview-section">
                        <h3>{{ $options.i18n.previewHeading }}</h3>
                        <p>{{ $options.i18n.previewInfo }}</p>

                        <div class="voter-preview-item">
                            <strong>{{ $options.i18n.votingFor }}</strong>
                            <ul>
                                <li v-for="name in selectedEntryNames" :key="name">{{ name }}</li>
                            </ul>
                        </div>

                        <div class="voter-preview-item">
                            <strong>{{ $options.i18n.voteContent }}</strong>
                            <pre class="voter-preview-code">{{ previewVoteText }}</pre>
                        </div>
                    </div>
                </cdx-dialog>
            `
      });
      registerCodexComponents(app, Codex);
      mountApp(app);
    }).catch((error) => {
      console.error("[Voter] 無法加載 Codex:", error);
      mw.notify(state_default.convByVar({ hant: "無法加載對話框組件。", hans: "无法加载对话框组件。" }), {
        type: "error",
        title: "[Voter]"
      });
    });
  }
  function openVoteDialog(sectionID) {
    if (getMountedApp && getMountedApp()) removeDialogMount();
    createVoteDialog(sectionID);
  }
  window.openVoteDialog = openVoteDialog;

  // src/dom.ts
  function addVoteButtons() {
    if (document.querySelector("#voter-finished-loading")) {
      return;
    }
    state_default.sectionTitles = [];
    let headingSelector;
    if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
      headingSelector = "div.mw-heading.mw-heading4";
    } else {
      headingSelector = "div.mw-heading.mw-heading2";
    }
    $(headingSelector).each((index, element) => {
      let $element = $(element);
      let anchor;
      if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
        anchor = $element.nextUntil(headingSelector, "ul").find("li .anchor").attr("id");
      } else {
        anchor = $element.find("h2").attr("id");
      }
      if (anchor) {
        let sectionID = getSectionID(index + 1);
        const $voteLink = $("<a>").text(state_default.convByVar({ hant: "投票", hans: "投票" })).css({ "cursor": "pointer", "margin-left": "0.25em" });
        $voteLink.on("click", (e) => {
          e.preventDefault();
          openVoteDialog(sectionID);
        });
        $('<span class="mw-editsection-bracket">|</span> ').insertAfter($element.find("span.mw-editsection > a").first());
        $voteLink.insertAfter($element.find("span.mw-editsection > a").first().next());
        state_default.sectionTitles.push({ data: sectionID, label: anchor.replace(/_/g, " ") });
      }
    });
    console.log(`[Voter] 已識別可投票事項共 ${state_default.sectionTitles.length} 項。`);
    let finishedLoading = document.createElement("div");
    finishedLoading.id = "voter-finished-loading";
    finishedLoading.style.display = "none";
    document.querySelector("#mw-content-text .mw-parser-output").appendChild(finishedLoading);
  }
  function getSectionID(childid) {
    try {
      let $heading;
      if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
        $heading = $("div.mw-heading.mw-heading4").eq(childid - 1);
      } else {
        $heading = $("div.mw-heading.mw-heading2").eq(childid - 1);
      }
      let $editlink = $heading.find("span.mw-editsection > a");
      let href = $editlink.attr("href");
      if (!href) throw new Error("No href found");
      let match = href.match(/section=(\\d+)/);
      if (match) return +match[1];
      let parts = href.split("&");
      for (let part of parts) {
        if (part.startsWith("section=")) return +part.split("=")[1].replace(/^T-/, "");
      }
    } catch (e) {
      console.log(`[Voter] Failed to get section ID for child ${childid}`);
      throw e;
    }
    return 0;
  }
  function titleVariants(title) {
    let us = title.replace(/ /g, "_");
    let sp = title.replace(/_/g, " ");
    return [title, us, sp, us.charAt(0).toUpperCase() + us.slice(1), sp.charAt(0).toUpperCase() + sp.slice(1)];
  }
  function textMatchTitleVariants(text, title) {
    return titleVariants(title).some((variant) => text.includes(variant));
  }
  function addIndent(text, indent) {
    return text.replace(/^/gm, indent);
  }
  function refreshPage(entryName) {
    location.href = mw.util.getUrl(state_default.pageName + "#" + entryName);
    location.reload();
  }
  async function vote(voteIDs, templates, message, useBulleted) {
    var _a;
    let VTReason = templates.map((str) => `{{${str}}}`).join(";");
    message = message.trim();
    VTReason += message ? ":" + message : "。";
    VTReason += "--~~~~";
    for (const id of voteIDs) {
      let votedPageName = ((_a = state_default.sectionTitles.find((x) => x.data === id)) == null ? void 0 : _a.label) || `section ${id}`;
      let indent = useBulleted ? "* " : ": ";
      let destPage = state_default.pageName;
      if (state_default.pageName === "Wikipedia:新条目推荐/候选") {
        indent = useBulleted ? "** " : "*: ";
      } else if (state_default.pageName === "Wikipedia:優良條目評選") {
        destPage += "/提名區";
      } else if (/^Wikipedia:(典范条目评选|特色列表评选)$/i.test(state_default.pageName)) {
        destPage += "/提名区";
      }
      let text = addIndent(VTReason, indent);
      let summary = `/* ${votedPageName} */ `;
      summary += templates.join("、");
      summary += " ([[User:SuperGrey/gadgets/voter|Voter]])";
      if (await voteAPI(state_default.pageName, destPage, id, text, summary)) return true;
    }
    setTimeout(() => {
      var _a2;
      return refreshPage((_a2 = state_default.sectionTitles.find((x) => x.data === voteIDs[0])) == null ? void 0 : _a2.label);
    }, 1e3);
    return false;
  }

  // src/styles.css
  var styles_default = "/* Voter Gadget Styles */\r\n\r\n/* Multi-step Dialog */\r\n.voter-dialog {\r\n    max-width: 600px;\r\n}\r\n\r\n.voter-multistep-dialog__header-top {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: space-between;\r\n    padding: 16px 24px 0;\r\n}\r\n\r\n.voter-multistep-dialog__header-top h2 {\r\n    margin: 0;\r\n    font-size: 18px;\r\n    font-weight: 600;\r\n}\r\n\r\n.voter-multistep-dialog__stepper {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 12px;\r\n    padding: 12px 24px;\r\n    border-bottom: 1px solid rgba(0, 0, 0, 0.1);\r\n}\r\n\r\n.voter-multistep-dialog__stepper__label {\r\n    font-size: 13px;\r\n    color: #54595d;\r\n    min-width: 36px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__steps {\r\n    display: flex;\r\n    gap: 6px;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step {\r\n    display: block;\r\n    width: 12px;\r\n    height: 12px;\r\n    border-radius: 999px;\r\n    background-color: #c8ccd1;\r\n    transition: background-color 0.2s ease;\r\n}\r\n\r\n.voter-multistep-dialog__stepper__step--active {\r\n    background-color: #36c;\r\n}\r\n\r\n/* Form Sections */\r\n.voter-form-section {\r\n    padding: 16px 0;\r\n}\r\n\r\n.voter-form-label {\r\n    display: block;\r\n    font-weight: 600;\r\n    margin-bottom: 8px;\r\n    font-size: 14px;\r\n}\r\n\r\n.voter-checkbox-grid {\r\n    display: grid;\r\n    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\r\n    gap: 0 8px;\r\n}\r\n\r\n/* Entry Info */\r\n.voter-entry-info {\r\n    margin-top: 12px;\r\n    padding: 12px;\r\n    background-color: #f8f9fa;\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    font-size: 13px;\r\n    line-height: 1.5;\r\n}\r\n\r\n.voter-entry-info--loading {\r\n    color: #72777d;\r\n    font-style: italic;\r\n}\r\n\r\n/* Preview Section */\r\n.voter-preview-section h3 {\r\n    margin: 0 0 12px;\r\n    font-size: 16px;\r\n    font-weight: 600;\r\n}\r\n\r\n.voter-preview-section p {\r\n    margin: 0 0 16px;\r\n    color: #54595d;\r\n}\r\n\r\n.voter-preview-item {\r\n    margin-bottom: 16px;\r\n}\r\n\r\n.voter-preview-item strong {\r\n    display: block;\r\n    margin-bottom: 8px;\r\n    font-size: 14px;\r\n}\r\n\r\n.voter-preview-item ul {\r\n    margin: 0;\r\n    padding-left: 24px;\r\n}\r\n\r\n.voter-preview-item li {\r\n    font-size: 14px;\r\n    line-height: 1.6;\r\n}\r\n\r\n.voter-preview-code {\r\n    padding: 12px;\r\n    background-color: #f8f9fa;\r\n    border: 1px solid #eaecf0;\r\n    border-radius: 4px;\r\n    font-family: monospace;\r\n    font-size: 13px;\r\n    white-space: pre-wrap;\r\n    word-break: break-word;\r\n    margin: 0;\r\n}\r\n\r\n/* Dialog Footer - override Codex defaults */\r\n.voter-dialog footer {\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: flex-end;\r\n    gap: 12px;\r\n    border-top: 1px solid rgba(0, 0, 0, 0.06);\r\n    padding: 12px 24px;\r\n}\r\n";

  // src/main.ts
  function injectStyles(css) {
    if (!css) return;
    try {
      const styleEl = document.createElement("style");
      styleEl.appendChild(document.createTextNode(css));
      document.head.appendChild(styleEl);
    } catch (e) {
      const div = document.createElement("div");
      div.innerHTML = `<style>${css}</style>`;
      document.head.appendChild(div.firstChild);
    }
  }
  function validatePage(pageName) {
    let validPages = [
      {
        name: "Wikipedia:新条目推荐/候选",
        templates: [
          { data: "支持", label: "支持" },
          { data: "反對", label: "反對" },
          { data: "不合要求", label: "不合要求" },
          { data: "問題不當", label: "問題不當" }
        ]
      },
      {
        name: "Wikipedia:優良條目評選",
        templates: [
          { data: "yesGA", label: "符合優良條目標準" },
          { data: "noGA", label: "不符合優良條目標準" }
        ]
      },
      {
        name: "Wikipedia:典范条目评选",
        templates: [
          { data: "yesFA", label: "符合典範條目標準" },
          { data: "noFA", label: "不符合典範條目標準" }
        ]
      },
      {
        name: "Wikipedia:特色列表评选",
        templates: [
          { data: "yesFL", label: "符合特色列表標準" },
          { data: "noFL", label: "不符合特色列表標準" }
        ]
      }
    ];
    for (let page of validPages) {
      if (pageName === page.name || new RegExp(`^${page.name}/`, "i").test(pageName)) {
        state_default.validVoteTemplates = page.templates;
        state_default.invalidVoteTemplates = [
          "中立",
          "意見",
          "建議",
          "疑問",
          "同上",
          "提醒"
        ].map((template) => ({
          data: template,
          label: template
        }));
        return true;
      }
    }
    return false;
  }
  function init() {
    if (typeof document !== "undefined") {
      injectStyles(styles_default);
    }
    if (!validatePage(state_default.pageName)) {
      console.log("[Voter] 不是目標頁面,小工具終止。");
      return;
    }
    state_default.initHanAssist().then(() => {
      console.log(`[Voter] 已載入,當前頁面為 ${state_default.pageName}。`);
      mw.hook("wikipage.content").add(function() {
        setTimeout(() => addVoteButtons(), 200);
      });
    });
  }
  init();
})();
// </nowiki>