diff --git a/src/cli.ts b/src/cli.ts index b90d632..79c6f16 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -838,7 +838,10 @@ async function main(): Promise { "--preset ", "pill-dark, pill-light, rounded-card, minimal-bar, or voice-orb", ) - .option("--position ", "bottom-right or bottom-left") + .option( + "--position ", + "bottom-right, bottom-left, top-right, top-left, bottom-center, top-center, center-right, center-left, or custom", + ) .option("--mode ", "chat or voice") .option("--launcher-label ", "Launcher button label") .option("--greeting ", "Initial greeting text") diff --git a/src/commands/projects/widget/commands.test.ts b/src/commands/projects/widget/commands.test.ts index 24e4c79..56ddbf8 100644 --- a/src/commands/projects/widget/commands.test.ts +++ b/src/commands/projects/widget/commands.test.ts @@ -5,6 +5,7 @@ import { runProjectsWidgetSet, runProjectsWidgetShow, validateWidgetHexColor, + validateWidgetPosition, validateWidgetPreset, } from "./commands.js"; @@ -125,6 +126,26 @@ describe("projects widget commands", () => { ); }); + it("sets top-left position", async () => { + getProjectWidget.mockResolvedValue(sampleWidgetResponse); + updateProjectWidgetDraft.mockResolvedValue({ + ...sampleWidgetResponse, + draft: { + ...sampleWidgetResponse.draft, + position: "top-left", + }, + }); + + await runProjectsWidgetSet({ position: "top-left" }); + + expect(updateProjectWidgetDraft).toHaveBeenCalledWith("proj-1", { + v: 1, + preset: "pill-dark", + position: "top-left", + mode: "voice", + }); + }); + it("rejects unknown preset", async () => { await expect( runProjectsWidgetSet({ preset: "unknown-preset" }), @@ -132,6 +153,13 @@ describe("projects widget commands", () => { expect(getProjectWidget).not.toHaveBeenCalled(); }); + it("rejects unknown position", async () => { + await expect( + runProjectsWidgetSet({ position: "center-stage" }), + ).rejects.toThrow(/Unknown widget position/); + expect(getProjectWidget).not.toHaveBeenCalled(); + }); + it("rejects bad hex colors", async () => { getProjectWidget.mockResolvedValue(sampleWidgetResponse); @@ -162,6 +190,17 @@ describe("projects widget commands", () => { /Unknown widget preset/, ); }); + + it("accepts top-left and custom positions", () => { + expect(validateWidgetPosition("top-left")).toBe("top-left"); + expect(validateWidgetPosition("custom")).toBe("custom"); + }); + + it("rejects unknown positions", () => { + expect(() => validateWidgetPosition("middle")).toThrow( + /Unknown widget position/, + ); + }); }); describe("runProjectsWidgetDeploy", () => { diff --git a/src/commands/projects/widget/commands.ts b/src/commands/projects/widget/commands.ts index 5669da0..1d91369 100644 --- a/src/commands/projects/widget/commands.ts +++ b/src/commands/projects/widget/commands.ts @@ -21,7 +21,17 @@ export const WIDGET_PRESETS = [ "voice-orb", ] as const; -export const WIDGET_POSITIONS = ["bottom-right", "bottom-left"] as const; +export const WIDGET_POSITIONS = [ + "bottom-right", + "bottom-left", + "top-right", + "top-left", + "bottom-center", + "top-center", + "center-right", + "center-left", + "custom", +] as const; export const WIDGET_MODES = ["chat", "voice"] as const; diff --git a/src/lib/api.ts b/src/lib/api.ts index b59b6df..922c56e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -678,6 +678,40 @@ export interface SessionConversationResponse { export type ProjectWidgetPublishStatus = "idle" | "queued" | "publishing" | "published" | "failed"; +export type WidgetMessageRoleTheme = { + fontFamily?: string; + fontSize?: string; + color?: string; + bubble?: string; +}; + +export type VoiceThereWidgetChatTheme = { + incoming?: WidgetMessageRoleTheme; + outgoing?: WidgetMessageRoleTheme; + headerBackground?: string; + inputBackground?: string; + inputColor?: string; + panelWidth?: string; + panelHeight?: string; + panelRadius?: string; +}; + +export type VoiceThereWidgetTheme = { + primary?: string; + background?: string; + text?: string; + fontFamily?: string; + fontSize?: string; + chat?: VoiceThereWidgetChatTheme; +}; + +export type WidgetPositionOffset = { + top?: string; + right?: string; + bottom?: string; + left?: string; +}; + export type VoiceThereWidgetConfigV1 = { v: 1; publicId?: string; @@ -686,10 +720,21 @@ export type VoiceThereWidgetConfigV1 = { revision?: number; preset?: "pill-dark" | "pill-light" | "rounded-card" | "minimal-bar" | "voice-orb"; - theme?: { primary?: string; background?: string; text?: string }; + theme?: VoiceThereWidgetTheme; launcherLabel?: string; greeting?: string; - position?: "bottom-right" | "bottom-left"; + position?: + | "bottom-right" + | "bottom-left" + | "top-right" + | "top-left" + | "bottom-center" + | "top-center" + | "center-right" + | "center-left" + | "custom"; + positionOffset?: WidgetPositionOffset; + customCss?: string; mode?: "chat" | "voice"; };