mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Refine selection rewrite frames
This commit is contained in:
parent
f7c91d880a
commit
9071fb238c
4 changed files with 134 additions and 42 deletions
|
|
@ -91,6 +91,9 @@ export class SelectionRewritePopover {
|
|||
this.cancel();
|
||||
}
|
||||
});
|
||||
this.addDomListener(activeDocument, "pointerdown", (event) => {
|
||||
if (!this.elements?.root.contains(event.target as Node | null)) this.cancel();
|
||||
});
|
||||
|
||||
this.setStatus("");
|
||||
this.syncInstructionHeight();
|
||||
|
|
@ -252,7 +255,7 @@ export class SelectionRewritePopover {
|
|||
private syncInstructionHeight(): void {
|
||||
const instruction = this.elements?.instruction ?? null;
|
||||
syncTextareaHeight(instruction, {
|
||||
minHeightFallback: 56,
|
||||
minHeightFallback: 26,
|
||||
maxHeightFallback: instruction ? Math.min(180, instruction.win.innerHeight * 0.3) : 180,
|
||||
});
|
||||
}
|
||||
|
|
@ -284,9 +287,6 @@ export class SelectionRewritePopover {
|
|||
onApply={() => {
|
||||
this.apply();
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.cancel();
|
||||
}}
|
||||
onGenerate={() => void this.generate()}
|
||||
onInstructionInput={(value) => {
|
||||
this.instructionDraft = value;
|
||||
|
|
@ -455,7 +455,6 @@ interface SelectionRewritePopoverViewProps {
|
|||
instructionRef: (element: HTMLTextAreaElement | null) => void;
|
||||
onApply: () => void;
|
||||
onApplyKeyDown: (event: TargetedKeyboardEvent<HTMLButtonElement>) => void;
|
||||
onCancel: () => void;
|
||||
onGenerate: () => void;
|
||||
onInstructionInput: (value: string) => void;
|
||||
onInstructionKeyDown: (event: TargetedKeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
|
|
@ -474,7 +473,6 @@ function SelectionRewritePopoverView({
|
|||
instructionRef,
|
||||
onApply,
|
||||
onApplyKeyDown,
|
||||
onCancel,
|
||||
onGenerate,
|
||||
onInstructionInput,
|
||||
onInstructionKeyDown,
|
||||
|
|
@ -484,18 +482,18 @@ function SelectionRewritePopoverView({
|
|||
return (
|
||||
<>
|
||||
<div className="codex-panel-selection-rewrite__prompt-row">
|
||||
<textarea
|
||||
ref={instructionRef}
|
||||
className="codex-panel-ui__text-input codex-panel-selection-rewrite__instruction"
|
||||
disabled={generating}
|
||||
onInput={(event) => {
|
||||
onInstructionInput(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={onInstructionKeyDown}
|
||||
placeholder="How should Codex rewrite this selection?"
|
||||
value={instruction}
|
||||
/>
|
||||
<div className="codex-panel-ui__action-stack codex-panel-selection-rewrite__controls">
|
||||
<div className="codex-panel-selection-rewrite__composer-frame">
|
||||
<textarea
|
||||
ref={instructionRef}
|
||||
className="codex-panel-ui__text-input codex-panel-selection-rewrite__instruction"
|
||||
disabled={generating}
|
||||
onInput={(event) => {
|
||||
onInstructionInput(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={onInstructionKeyDown}
|
||||
placeholder="How should Codex rewrite this selection?"
|
||||
value={instruction}
|
||||
/>
|
||||
<IconButton
|
||||
icon="sparkles"
|
||||
label={hasReplacement ? "Regenerate" : "Generate"}
|
||||
|
|
@ -503,29 +501,25 @@ function SelectionRewritePopoverView({
|
|||
disabled={generating || !hasInstruction}
|
||||
onClick={onGenerate}
|
||||
/>
|
||||
<IconButton
|
||||
icon="x"
|
||||
label="Cancel"
|
||||
className="clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SelectionRewriteStatus status={status} />
|
||||
<pre className={`codex-panel-selection-rewrite__stream-preview${streamPreview ? "" : " is-hidden"}`}>{streamPreview}</pre>
|
||||
<div className={`codex-panel-selection-rewrite__result-row${hasReplacement ? "" : " is-hidden"}`}>
|
||||
<div className={`codex-panel-selection-rewrite__result${hasReplacement ? "" : " is-hidden"}`}>
|
||||
<div className="codex-panel-selection-rewrite__diff">{diff ? <SelectionRewriteDiff diff={diff} /> : null}</div>
|
||||
<IconButton
|
||||
buttonRef={applyButtonRef}
|
||||
icon="check"
|
||||
label="Apply"
|
||||
className={`clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button${
|
||||
hasReplacement ? "" : " is-hidden"
|
||||
}`}
|
||||
disabled={generating || !hasReplacement}
|
||||
onClick={onApply}
|
||||
onKeyDown={onApplyKeyDown}
|
||||
/>
|
||||
<div className="codex-panel-selection-rewrite__result-actions">
|
||||
<IconButton
|
||||
buttonRef={applyButtonRef}
|
||||
icon="check"
|
||||
label="Apply"
|
||||
className={`clickable-icon codex-panel-ui__icon-button codex-panel-selection-rewrite__icon-button${
|
||||
hasReplacement ? "" : " is-hidden"
|
||||
}`}
|
||||
disabled={generating || !hasReplacement}
|
||||
onClick={onApply}
|
||||
onKeyDown={onApplyKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{debugText ? (
|
||||
<details className="codex-panel-selection-rewrite__debug">
|
||||
|
|
|
|||
|
|
@ -20,33 +20,76 @@
|
|||
}
|
||||
|
||||
.codex-panel-selection-rewrite__prompt-row {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__composer-frame {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--codex-panel-section-gap);
|
||||
align-items: stretch;
|
||||
gap: var(--codex-panel-control-gap);
|
||||
min-width: 0;
|
||||
align-items: end;
|
||||
border: var(--input-border-width, var(--border-width, 1px)) solid var(--background-modifier-border);
|
||||
border-radius: var(--input-radius, var(--radius-s));
|
||||
background: var(--background-modifier-form-field);
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__composer-frame:focus-within {
|
||||
border-color: var(--background-modifier-border-focus);
|
||||
box-shadow: 0 0 0 var(--input-border-width, var(--border-width, 1px)) var(--background-modifier-border-focus);
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__instruction {
|
||||
min-height: var(--codex-panel-composer-min-height);
|
||||
min-height: var(--codex-panel-size-user-input-text-min-height);
|
||||
max-height: min(var(--codex-panel-size-selection-rewrite-preview-max-height), 30vh);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__result-row {
|
||||
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:hover,
|
||||
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:focus,
|
||||
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:focus-visible,
|
||||
.codex-panel-selection-rewrite__instruction.codex-panel-selection-rewrite__instruction:active {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__result {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--codex-panel-section-gap);
|
||||
gap: var(--codex-panel-control-gap);
|
||||
min-width: 0;
|
||||
align-items: start;
|
||||
border: var(--codex-panel-border);
|
||||
border-radius: var(--codex-panel-card-radius);
|
||||
background: var(--background-modifier-form-field);
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__result-row.is-hidden,
|
||||
.codex-panel-selection-rewrite__result.is-hidden,
|
||||
.codex-panel-selection-rewrite__diff:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__result-actions {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__icon-button.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__icon-button {
|
||||
min-width: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-panel-gap) * 2);
|
||||
margin: var(--codex-panel-panel-gap);
|
||||
padding: var(--codex-panel-panel-gap);
|
||||
}
|
||||
|
||||
.codex-panel-selection-rewrite__status {
|
||||
padding-inline-start: calc(var(--size-4-1) / 2);
|
||||
color: var(--codex-panel-text-muted);
|
||||
|
|
@ -101,6 +144,8 @@
|
|||
margin: 0;
|
||||
padding: var(--codex-panel-item-gap) var(--codex-panel-section-gap);
|
||||
overflow: auto;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@ describe("selection rewrite popover", () => {
|
|||
|
||||
openPopover(popover);
|
||||
expect(document.querySelector(".codex-panel-selection-rewrite__instruction")).not.toBeNull();
|
||||
expect(document.querySelector<HTMLButtonElement>('button[aria-label="Cancel"]')).toBeNull();
|
||||
|
||||
closePopover(popover);
|
||||
|
||||
|
|
@ -305,6 +306,27 @@ describe("selection rewrite popover", () => {
|
|||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("closes from outside pointerdown without closing internal controls", () => {
|
||||
const onClose = vi.fn();
|
||||
const popover = new SelectionRewritePopover(popoverOptions({ onClose }));
|
||||
|
||||
openPopover(popover);
|
||||
const generate = expectPresent(document.querySelector<HTMLButtonElement>('button[aria-label="Generate"]'));
|
||||
void act(() => {
|
||||
generate.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(document.querySelector(".codex-panel-selection-rewrite")).not.toBeNull();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
|
||||
void act(() => {
|
||||
document.body.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(document.querySelector(".codex-panel-selection-rewrite")).toBeNull();
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("aborts the active generation when closed and ignores its late result", async () => {
|
||||
const rewrite = deferred<{ replacementText: string }>();
|
||||
let signal: AbortSignal | undefined;
|
||||
|
|
|
|||
|
|
@ -256,6 +256,37 @@ describe("chat message CSS", () => {
|
|||
});
|
||||
|
||||
describe("selection rewrite CSS", () => {
|
||||
it("uses a composer-style instruction frame without the old action stack", () => {
|
||||
const frame = /\.codex-panel-selection-rewrite__composer-frame \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const instruction = /\.codex-panel-selection-rewrite__instruction \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const iconButton = /\.codex-panel-selection-rewrite__icon-button \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(frame).toContain("grid-template-columns: minmax(0, 1fr) auto");
|
||||
expect(frame).toContain("background: var(--background-modifier-form-field)");
|
||||
expect(frame).toContain("border: var(--input-border-width, var(--border-width, 1px)) solid var(--background-modifier-border)");
|
||||
expect(instruction).toContain("min-height: var(--codex-panel-size-user-input-text-min-height)");
|
||||
expect(instruction).toContain("background: transparent");
|
||||
expect(instruction).toContain("border: 0");
|
||||
expect(iconButton).toContain("min-width: calc(var(--codex-panel-control-icon-size) + var(--codex-panel-panel-gap) * 2)");
|
||||
expect(iconButton).toContain("margin: var(--codex-panel-panel-gap)");
|
||||
expect(iconButton).toContain("padding: var(--codex-panel-panel-gap)");
|
||||
expect(styles).not.toContain(".codex-panel-selection-rewrite__controls");
|
||||
});
|
||||
|
||||
it("keeps the apply action inside the selection rewrite result frame", () => {
|
||||
const result = /\.codex-panel-selection-rewrite__result \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const actions = /\.codex-panel-selection-rewrite__result-actions \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
const diffBody = /\.codex-panel-selection-rewrite__diff-body \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
expect(result).toContain("grid-template-columns: minmax(0, 1fr) auto");
|
||||
expect(result).toContain("border: var(--codex-panel-border)");
|
||||
expect(result).toContain("background: var(--background-modifier-form-field)");
|
||||
expect(actions).toContain("padding: 0");
|
||||
expect(diffBody).toContain("margin: 0");
|
||||
expect(diffBody).toContain("border-radius: 0");
|
||||
expect(diffBody).toContain("background: transparent");
|
||||
});
|
||||
|
||||
it("aligns generation status text with the instruction input text inset", () => {
|
||||
const status = /\.codex-panel-selection-rewrite__status \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue