js/settingsTexts.mjs

/**
 * 設定彈窗(節點 / 連線)之顯示文字 —— 鍵、預設與回退之單一來源(內部模組, 不對外)。
 *
 * 對外介面一律逐字展開, 一句文字一個鍵(業主規範, 見 CLAUDE.md「opt 設計規範」):
 *   opt 鍵   = {nodes|conns}Settings + 文字 prop 名(首字大寫), 如 opt.nodesSettingsTextLabelName
 *   表單 prop = 文字 prop 名, 如 NodeSettingsForm 之 textLabelName
 * 文字 prop 名沿用 w-component-vue 之文字 prop 慣例:
 *   群標題     {群鍵}GroupTitle          (同 uploadModeTitle 之 {對象}Title)
 *   欄位標籤   textLabel{欄位鍵}          (同 textLabelDataName)
 *   下拉選項   {類別}TextFor{值}          (同 uploadModeTextForReplace; 類別 = 所服務之欄位鍵, 兩欄共用一張下拉者取共同字根
 *                                         position / marker; 值 kebab → PascalCase, 空字串之「無」為 None)
 *   按鈕文字   deleteText / colorConfirmText(deleteText 即既有之 opt.nodesSettingsDeleteText / connsSettingsDeleteText)
 *   圖示鈕提示 pointsAddBtnTooltip / pointsRemoveBtnTooltip(同 saveBtnTooltip; 亦作 aria-label)
 *   空狀態     pointsTextEmpty            (同 textEmpty)
 *   輸入框提示 pointsXTooltip / pointsYTooltip
 * 選項之鍵由值域模組產生(值域新增之值自動有鍵, 預設文字由值轉寫); 鍵集與 spec 之對照由單元測試把關。
 * 內部傳遞(WFlowVue → Renderer → Wrapper → 表單)以「prop 名 → 文字」物件下傳再展開到表單 prop, 屬內部機制。
 *
 * 回退: 非字串或空字串 → 預設; 純空白字串照收(與工具列 tooltip 同規則)。
 */
import { NODE_SETTING_GROUPS, CONN_SETTING_GROUPS } from './settingsGroups.mjs'
import { SHAPES } from './nodeStyle.mjs'
import { SIDES } from './anchorPolicy.mjs'
import { EDGE_TYPES } from './edgePath.mjs'
import { MARKER_TYPES } from './edgeMarker.mjs'

//兩表單共用之欄位標籤(同一件事在兩個 popup 用同一個字, 只定義一次)
const COMMON_LABELS = {
    name: 'Name',
    description: 'Description',
    edgeColor: 'Edge Color',
    edgeWidth: 'Edge Width',
    fontSize: 'Font Size',
    fontColor: 'Font Color',
    popupDirection: 'Popup Direction',
}

const NODE_FIELD_LABELS = {
    name: COMMON_LABELS.name,
    description: COMMON_LABELS.description,
    shape: 'Shape',
    faceColor: 'Face Color',
    edgeColor: COMMON_LABELS.edgeColor,
    edgeWidth: COMMON_LABELS.edgeWidth,
    fontSize: COMMON_LABELS.fontSize,
    fontColor: COMMON_LABELS.fontColor,
    popupDirection: COMMON_LABELS.popupDirection,
}

const CONN_FIELD_LABELS = {
    name: COMMON_LABELS.name,
    description: COMMON_LABELS.description,
    type: 'Type',
    fromPosition: 'From Anchor',
    toPosition: 'To Anchor',
    points: 'Waypoints',
    edgeColor: COMMON_LABELS.edgeColor,
    edgeWidth: COMMON_LABELS.edgeWidth,
    animated: 'Animated',
    markerFrom: 'From Marker',
    markerFromSize: 'From Marker Size',
    markerFromFaceColor: 'From Marker Face Color',
    markerFromEdgeColor: 'From Marker Edge Color',
    markerTo: 'To Marker',
    markerToSize: 'To Marker Size',
    markerToFaceColor: 'To Marker Face Color',
    markerToEdgeColor: 'To Marker Edge Color',
    fontSize: COMMON_LABELS.fontSize,
    fontColor: COMMON_LABELS.fontColor,
    popupDirection: COMMON_LABELS.popupDirection,
}

//下拉之選項類別 → 值域(值域單一來源在各值域模組, 此處只引用)
const NODE_OPTION_VALUES = { shape: SHAPES, popupDirection: SIDES }
const CONN_OPTION_VALUES = { type: EDGE_TYPES, position: SIDES, marker: MARKER_TYPES, popupDirection: SIDES }

//轉寫不出正確英文之值才收(不得擴成完整對照表: 值域新增之值自動以 Title Case 呈現)
const OPTION_TEXT_EXCEPTIONS = Object.freeze({
    smoothstep: 'Smooth Step',
    arrowclosed: 'Arrow Closed',
})

/** kebab-case 值 → 顯示文字(triangle-up → Triangle Up) */
export function titleCase(v) {
    return String(v).split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
}

//kebab / camel → PascalCase(triangle-up → TriangleUp, faceColor → FaceColor)
function pascal(v) {
    return String(v).split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('')
}

//選項值 → 鍵用之值: 空字串為「無」
function optionValueKey(value) {
    return value === '' ? 'none' : String(value)
}

/** 群標題之 prop 名(basic → basicGroupTitle) */
export function groupTitleProp(groupKey) {
    return groupKey + 'GroupTitle'
}

/** 欄位標籤之 prop 名(faceColor → textLabelFaceColor) */
export function fieldLabelProp(fieldKey) {
    return 'textLabel' + pascal(fieldKey)
}

/** 下拉選項之 prop 名(shape, triangle-up → shapeTextForTriangleUp; marker, '' → markerTextForNone) */
export function optionTextProp(category, value) {
    return category + 'TextFor' + pascal(optionValueKey(value))
}

/** 選項之預設文字: 例外表 → Title Case */
export function defaultOptionText(value) {
    const k = optionValueKey(value)
    return Object.prototype.hasOwnProperty.call(OPTION_TEXT_EXCEPTIONS, k) ? OPTION_TEXT_EXCEPTIONS[k] : titleCase(k)
}

function build(groups, fieldLabels, optionValues, buttons) {
    const t = {}
    for (const g of groups) t[groupTitleProp(g.key)] = g.title
    for (const k of Object.keys(fieldLabels)) t[fieldLabelProp(k)] = fieldLabels[k]
    for (const c of Object.keys(optionValues)) {
        for (const v of optionValues[c]) t[optionTextProp(c, v)] = defaultOptionText(v)
    }
    for (const k of Object.keys(buttons)) t[k] = buttons[k]
    return Object.freeze(t)
}

/** 節點設定彈窗: 文字 prop 名 → 英文預設(26 句) */
export const NODE_SETTINGS_TEXTS = build(NODE_SETTING_GROUPS, NODE_FIELD_LABELS, NODE_OPTION_VALUES, {
    deleteText: 'Delete',
    colorConfirmText: 'Confirm',
})

/** 連線設定彈窗: 文字 prop 名 → 英文預設(48 句) */
export const CONN_SETTINGS_TEXTS = build(CONN_SETTING_GROUPS, CONN_FIELD_LABELS, CONN_OPTION_VALUES, {
    pointsAddBtnTooltip: 'Add Waypoint',
    pointsRemoveBtnTooltip: 'Remove Waypoint',
    pointsTextEmpty: 'None (auto-routed)',
    pointsXTooltip: 'X',
    pointsYTooltip: 'Y',
    deleteText: 'Delete',
    colorConfirmText: 'Confirm',
})

function textsOf(kind) {
    return kind === 'conn' ? CONN_SETTINGS_TEXTS : NODE_SETTINGS_TEXTS
}

/** 文字 prop 名 → opt 鍵(node, textLabelName → nodesSettingsTextLabelName) */
export function settingsTextOptKey(kind, prop) {
    return (kind === 'conn' ? 'connsSettings' : 'nodesSettings') + prop.charAt(0).toUpperCase() + prop.slice(1)
}

/** OPT_SPEC 用: 每個文字 opt 鍵一筆 { kind: 'text', def } */
export function settingsTextOptSpec() {
    const r = {}
    for (const kind of ['node', 'conn']) {
        const t = textsOf(kind)
        for (const p of Object.keys(t)) r[settingsTextOptKey(kind, p)] = { kind: 'text', def: t[p] }
    }
    return r
}

/** 表單之文字 props(每句一個 String prop; 未給為空字串 → 回退預設) */
export function settingsTextProps(kind) {
    const r = {}
    for (const p of Object.keys(textsOf(kind))) r[p] = { type: String, default: '' }
    return r
}

function isText(v) {
    return typeof v === 'string' && v !== ''
}

/** 解析表單文字: 自 src(表單實例或任一物件)逐 prop 取值, 非字串或空字串回退預設; 回傳新物件 */
export function resolveSettingsTexts(kind, src) {
    const defs = textsOf(kind)
    const r = {}
    for (const p of Object.keys(defs)) {
        const v = src ? src[p] : undefined
        r[p] = isText(v) ? v : defs[p]
    }
    return r
}

/** WFlowVue 內部下傳用: 由各文字 opt 鍵之 computed 組成「表單 prop 名 → 文字」 */
export function pickSettingsTexts(kind, vm) {
    const r = {}
    for (const p of Object.keys(textsOf(kind))) r[p] = vm[settingsTextOptKey(kind, p)]
    return r
}