项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

145 行
4.3 KiB
TypeScript

/**
* Single source of truth for the timeline's cue/element taxonomy: per action
* type, the icon + i18n label key + glyph tint + card accent; the set of
* element-bound cue types; and element-type label keys. Previously these maps
* were duplicated across ActionsBar, regenerate-tool-ui and ElementPickLayer.
*
* Labels are i18n KEYS (resolved by the caller via `t()`), not literals, so the
* pure module stays hook-free while the UI text goes through `useTranslation`.
*/
import {
Circle,
Crosshair,
Flag,
Focus,
PenLine,
Presentation,
Quote,
Shapes,
Sigma,
Table2,
type LucideIcon,
} from 'lucide-react';
/** Translator fn (matches useI18n's `t`) — passed in so this module stays hook-free. */
type TFn = (key: string, options?: Record<string, unknown>) => string;
export interface CueMeta {
icon: LucideIcon;
/** i18n key under `edit.cue.*`; resolve with `t(labelKey)`. */
labelKey: string;
/** glyph tint: icon color + soft disc background */
glyph: string;
/** top accent bar tint for the cue card */
accent: string;
/** dashed-border tint used by a "needs target" cue card */
dash: string;
}
const META: Record<string, CueMeta> = {
speech: {
icon: Quote,
labelKey: 'edit.cue.speech',
glyph: 'text-primary bg-primary/10 dark:text-primary',
accent: 'bg-primary/40',
dash: 'border-primary/40',
},
spotlight: {
icon: Focus,
labelKey: 'edit.cue.spotlight',
glyph: 'text-amber-600 bg-amber-500/10 dark:text-amber-400',
accent: 'bg-amber-400/70',
dash: 'border-amber-400/70',
},
laser: {
icon: Crosshair,
labelKey: 'edit.cue.laser',
glyph: 'text-rose-600 bg-rose-500/10 dark:text-rose-400',
accent: 'bg-rose-400/70',
dash: 'border-rose-400/70',
},
wb_open: {
icon: Presentation,
labelKey: 'edit.cue.whiteboardOpen',
glyph: 'text-sky-600 bg-sky-500/10 dark:text-sky-400',
accent: 'bg-sky-400/70',
dash: 'border-primary/40',
},
wb_draw_text: {
icon: PenLine,
labelKey: 'edit.cue.whiteboardText',
glyph: 'text-sky-600 bg-sky-500/10 dark:text-sky-400',
accent: 'bg-sky-400/70',
dash: 'border-primary/40',
},
wb_draw_shape: {
icon: Shapes,
labelKey: 'edit.cue.whiteboardShape',
glyph: 'text-sky-600 bg-sky-500/10 dark:text-sky-400',
accent: 'bg-sky-400/70',
dash: 'border-primary/40',
},
wb_draw_latex: {
icon: Sigma,
labelKey: 'edit.cue.whiteboardLatex',
glyph: 'text-sky-600 bg-sky-500/10 dark:text-sky-400',
accent: 'bg-sky-400/70',
dash: 'border-primary/40',
},
wb_draw_table: {
icon: Table2,
labelKey: 'edit.cue.whiteboardTable',
glyph: 'text-sky-600 bg-sky-500/10 dark:text-sky-400',
accent: 'bg-sky-400/70',
dash: 'border-primary/40',
},
discussion: {
icon: Flag,
labelKey: 'edit.cue.discussion',
glyph: 'text-yellow-600 bg-yellow-500/10 dark:text-yellow-400',
accent: 'bg-yellow-400/70',
dash: 'border-primary/40',
},
};
const FALLBACK: CueMeta = {
icon: Circle,
labelKey: 'edit.cue.action',
glyph: 'text-muted-foreground bg-muted',
accent: 'bg-muted-foreground/30',
dash: 'border-muted-foreground/30',
};
export function cueMeta(type: string): CueMeta {
return META[type] ?? FALLBACK;
}
/** Localized cue label; unknown types fall back to the raw type string. */
export function cueLabel(type: string, t: TFn): string {
return META[type] ? t(META[type].labelKey) : type;
}
/** Cue types that target a canvas element (so canvas pick mode applies). */
export const ELEMENT_BOUND = new Set(['spotlight', 'laser', 'play_video']);
const EL_TYPE_KEY: Record<string, string> = {
text: 'edit.element.text',
image: 'edit.element.image',
shape: 'edit.element.shape',
line: 'edit.element.line',
chart: 'edit.element.chart',
table: 'edit.element.table',
latex: 'edit.element.latex',
video: 'edit.element.video',
audio: 'edit.element.audio',
code: 'edit.element.code',
};
/** Human label for a slide element (localized type + a short content snippet). */
export function elementLabel(el: { type: string; content?: string }, t: TFn): string {
const typeLabel = EL_TYPE_KEY[el.type] ? t(EL_TYPE_KEY[el.type]) : el.type;
const raw = (el.content ?? '').replace(/<[^>]+>/g, '').trim();
const snip = raw ? ` · ${raw.slice(0, 16)}${raw.length > 16 ? '…' : ''}` : '';
return `${typeLabel}${snip}`;
}