// Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs". /** * Family-shared chrome for plugin settings cards: a disclosure header naming * the plugin and what its settings govern, the controls inside, and the save * that writes them. Renders nothing while the namespace is unavailable — a * deployment that does not compose the owning plugin should show no trace of * it. Inlined into each consumer's client bundle; mirrors the official * ui-plugin-config PluginCard in a self-contained slice. */ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react' import type { CardShell } from './settings-form.ts' import css from './settings-card.module.css' /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */ export const CARD_COPY_KEYS = [ 'settings.collapse', 'settings.expand', 'settings.notExposed', 'settings.unsaved', 'settings.readOnly', 'settings.saveFailed', 'settings.discard', 'settings.save', 'settings.saving', ] as const /** Copy key the card chrome itself reads. */ export type CardCopyKey = (typeof CARD_COPY_KEYS)[number] /** Key domain for the plugin's own copy (inferred per consumer). */ export type SettingsCardKey = TKey | CardCopyKey /** Card chrome shared by every plugin settings card. */ export interface PluginSettingsCardProps { /** Locale reader for this card's copy. */ t: (key: SettingsCardKey, params?: Record) => string /** Locale key of the plugin's name. */ titleKey: TKey /** Locale key of the line describing what this plugin's settings govern. */ descriptionKey: TKey /** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */ descriptionNode?: ReactNode /** The card's form state: availability, writability, and what a save would do. */ state: CardShell /** Write every staged edit. */ onSave: () => void /** Drop every staged edit. */ onDiscard: () => void /** * Render the controls expanded on arrival (still collapsible). Defaults to * true: promoted first-level sections show their settings immediately. */ defaultOpen?: boolean /** * Render without the collapse affordance: a static header and an always * visible body. Used by first-level settings sections whose nav entry * already provides the selection. */ alwaysOpen?: boolean /** * Hide the save/discard footer: cards whose body applies its own changes * immediately (an embedded external settings section) have no staged * edits, so the footer would sit there permanently disabled. */ hideFooter?: boolean /** The plugin's controls. */ children: ReactNode } /** * Render one plugin settings card. * @param props - the plugin's copy keys, its form state, and its controls. * @returns the card, or nothing while the namespace is still loading. */ export function PluginSettingsCard(props: PluginSettingsCardProps) { const [open, setOpen] = useState(props.defaultOpen ?? true) const { state, alwaysOpen } = props if (!state.available) return null const title = props.t(props.titleKey) const description = props.t(props.descriptionKey) const blocked = !state.dirty || state.invalid || state.saving const expanded = alwaysOpen === true || open const cardClass = expanded ? `${css.cardOpen} ${css.card}` : css.card // With alwaysOpen the nav entry already provides the selection, so the // header is a static title row instead of a disclosure button. const header = alwaysOpen === true ? (
{title} {props.descriptionNode ?? description} {state.dirty ? {props.t('settings.unsaved')} : null}
) : ( ) // The namespace exists but the Host does not serve it to this client (the // official settings allowlist omits third-party namespaces): show a card // that explains the gap instead of vanishing, so a missing card never // reads as a missing plugin. if (!state.exposed) { return (
  • {header} {expanded ? (

    {props.t('settings.notExposed')}

    ) : null}
  • ) } return (
  • {header} {expanded ? (
    {!state.writable ?

    {props.t('settings.readOnly')}

    : null} {props.children} {props.hideFooter === true ? null : (
    {state.failed ? (

    {props.t('settings.saveFailed')}{state.failedReason ? ' - ' + state.failedReason : ''}

    ) : null}
    )}
    ) : null}
  • ) } /** Props every field control needs regardless of its value type. */ export interface FieldProps { /** Stable id associating the label with its control. */ id: string /** Visible label. */ label: string /** One-line explanation rendered under the control. */ hint: string /** Draft text this control renders. */ text: string /** True when saving would leave a user-layer entry for this field. */ overridden: boolean /** True when the draft is not a value this field accepts. */ invalid: boolean /** Copy for the overridden badge. */ overriddenLabel: string /** Copy for the reset control. */ resetLabel: string /** Copy shown in place of the hint while the draft is invalid. */ invalidLabel: string /** Disables every control (read-only document, or an unavailable namespace). */ disabled: boolean /** Stage draft text. */ onEdit: (text: string) => void /** Stage a clear so the field re-inherits the composition layer. */ onReset: () => void } /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */ export function ValueField(props: FieldProps & { /** Hints a numeric keypad without narrowing what the control accepts. */ numeric?: boolean /** Placeholder shown while the draft is empty. */ placeholder?: string }) { return (
    {props.overridden ? ( {props.overriddenLabel} ) : null}
    { props.onEdit(event.target.value) }} />

    {props.invalid ? props.invalidLabel : props.hint}

    ) } const NON_SKIN_BODY_MARKERS = new Set(['dshSkinCenter', 'dshSidebarCollapsed']) // 检测使用使用皮肤,用了皮肤退回原生select样式,防止样式冲突。默认外观下使用优化后的select样式。 function isSkinActive(): boolean { const datasetList = Object.keys(document.body.dataset) const isActive = datasetList.some(key => key.startsWith('dsh') && !NON_SKIN_BODY_MARKERS.has(key)) return isActive } interface SelectOption { value: string label: string } const SELECT_CLOSE_MS = 100 /** * The shared dual-mode select control. While an appearance skin is active it * renders the legacy native ` { props.onEdit(event.target.value) }} > {options.map(option => ( ))} ) } const label = options.find(option => option.value === value)?.label ?? '' const popupClass = closing ? `${css.selectPopup} ${css.selectPopupClose}` : phase === 'open' ? `${css.selectPopup} ${css.selectPopupOpen}` : css.selectPopup return (
    {open ? (
    {options.map((option, index) => (
    { commit(index) }} > {option.label}
    ))}
    ) : null}
    ) } /** A staged boolean field: 继承 / 开 / 关. */ export function BooleanField(props: FieldProps & { /** Copy for the inherit option. */ inheritLabel: string /** Copy for the on option. */ onLabel: string /** Copy for the off option. */ offLabel: string }) { return (
    {props.overridden ? ( {props.overriddenLabel} ) : null}

    {props.hint}

    ) } /** A staged enumerated field rendered as a select. */ export function ChoiceField(props: FieldProps & { /** Copy for the inherit option (draft text is the empty string). */ inheritLabel: string /** Choices rendered in order; `value` is the draft/stored text. */ choices: ReadonlyArray<{ value: string; label: string }> }) { return (
    {props.overridden ? ( {props.overriddenLabel} ) : null}

    {props.invalid ? props.invalidLabel : props.hint}

    ) }