mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Align settings controls with Obsidian components
This commit is contained in:
parent
e548d70356
commit
34973dd7d7
3 changed files with 71 additions and 68 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import type { TargetedEvent, TargetedKeyboardEvent, ComponentChild as UiNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import { DEFAULT_CODEX_PATH } from "../constants";
|
||||
import { IconButton } from "../shared/ui/components.obsidian";
|
||||
import { IconButton, ObsidianCommitTextInput, ObsidianDropdown, ObsidianToggle } from "../shared/ui/components.obsidian";
|
||||
import type { SendShortcut } from "../shared/ui/keyboard";
|
||||
import { ArchivedThreadSection } from "./archived-section";
|
||||
import { HelperSettingsSection } from "./helper-section";
|
||||
|
|
@ -14,6 +13,10 @@ const SEND_SHORTCUT_LABELS = {
|
|||
enter: "Enter",
|
||||
"mod-enter": "Cmd/Ctrl+Enter",
|
||||
} as const;
|
||||
const SEND_SHORTCUT_OPTIONS: { value: SendShortcut; label: string }[] = [
|
||||
{ value: "enter", label: SEND_SHORTCUT_LABELS.enter },
|
||||
{ value: "mod-enter", label: SEND_SHORTCUT_LABELS["mod-enter"] },
|
||||
];
|
||||
|
||||
interface SettingsTabPanelState {
|
||||
codexPath: string;
|
||||
|
|
@ -75,10 +78,10 @@ function PanelPreferenceSections({ panel, actions }: { panel: SettingsTabPanelSt
|
|||
<SettingsGroup className="codex-panel-settings__section codex-panel-settings__general-section">
|
||||
<SettingsItems>
|
||||
<SettingRow name="Codex executable" desc="Command used to start `codex app-server`; use an absolute path if needed.">
|
||||
<CommitTextInput value={panel.codexPath} placeholder={DEFAULT_CODEX_PATH} onCommit={actions.setCodexPath} />
|
||||
<ObsidianCommitTextInput value={panel.codexPath} placeholder={DEFAULT_CODEX_PATH} onCommit={actions.setCodexPath} />
|
||||
</SettingRow>
|
||||
<SettingRow name="Show chat toolbar" desc="Show the toolbar above the chat panel.">
|
||||
<SettingsCheckbox checked={panel.showToolbar} onChange={actions.setShowToolbar} />
|
||||
<ObsidianToggle checked={panel.showToolbar} onChange={actions.setShowToolbar} />
|
||||
</SettingRow>
|
||||
</SettingsItems>
|
||||
</SettingsGroup>
|
||||
|
|
@ -86,72 +89,22 @@ function PanelPreferenceSections({ panel, actions }: { panel: SettingsTabPanelSt
|
|||
<SettingsHeading name="Composer" />
|
||||
<SettingsItems>
|
||||
<SettingRow name="Send shortcut" desc="Pick Enter or Cmd/Ctrl+Enter. Shift+Enter adds a newline when Enter sends.">
|
||||
<select
|
||||
<ObsidianDropdown
|
||||
value={panel.sendShortcut}
|
||||
onChange={(event) => {
|
||||
actions.setSendShortcut(event.currentTarget.value === "mod-enter" ? "mod-enter" : "enter");
|
||||
onChange={(value) => {
|
||||
actions.setSendShortcut(value === "mod-enter" ? "mod-enter" : "enter");
|
||||
}}
|
||||
>
|
||||
<option value="enter">{SEND_SHORTCUT_LABELS.enter}</option>
|
||||
<option value="mod-enter">{SEND_SHORTCUT_LABELS["mod-enter"]}</option>
|
||||
</select>
|
||||
options={SEND_SHORTCUT_OPTIONS}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name="Scroll thread from composer line edges"
|
||||
desc="Use Up/Ctrl+P and Down/Ctrl+N at composer line edges to scroll the thread."
|
||||
>
|
||||
<SettingsCheckbox checked={panel.scrollThreadFromComposerEdges} onChange={actions.setScrollThreadFromComposerEdges} />
|
||||
<ObsidianToggle checked={panel.scrollThreadFromComposerEdges} onChange={actions.setScrollThreadFromComposerEdges} />
|
||||
</SettingRow>
|
||||
</SettingsItems>
|
||||
</SettingsGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CommitTextInput({
|
||||
value,
|
||||
placeholder,
|
||||
onCommit,
|
||||
}: {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onCommit: (value: string) => void;
|
||||
}): UiNode {
|
||||
const [draft, setDraft] = useState(value);
|
||||
useEffect(() => {
|
||||
setDraft(value);
|
||||
}, [value]);
|
||||
const commit = (nextValue = draft): void => {
|
||||
onCommit(nextValue);
|
||||
};
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={draft}
|
||||
placeholder={placeholder}
|
||||
onInput={(event) => {
|
||||
setDraft(event.currentTarget.value);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
commit(event.currentTarget.value);
|
||||
}}
|
||||
onKeyDown={(event: TargetedKeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
commit(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsCheckbox({ checked, onChange }: { checked: boolean; onChange: (value: boolean) => void }): UiNode {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(event: TargetedEvent<HTMLInputElement>) => {
|
||||
onChange(event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export interface ObsidianDropdownOption {
|
|||
label: string;
|
||||
}
|
||||
|
||||
function useLatestRef<T>(value: T): { current: T } {
|
||||
const ref = useRef(value);
|
||||
ref.current = value;
|
||||
return ref;
|
||||
}
|
||||
|
||||
function ObsidianIcon({ icon, className }: ObsidianIconProps): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
|
|
@ -115,6 +121,7 @@ export function ObsidianDropdown({
|
|||
onChange: (value: string) => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
const onChangeRef = useLatestRef(onChange);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
|
|
@ -124,12 +131,12 @@ export function ObsidianDropdown({
|
|||
dropdown.addOption(option.value, option.label);
|
||||
}
|
||||
dropdown.setValue(value).onChange((selected) => {
|
||||
onChange(selected);
|
||||
onChangeRef.current(selected);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [onChange, options, value]);
|
||||
}, [onChangeRef, options, value]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
|
@ -144,6 +151,7 @@ export function ObsidianTextInput({
|
|||
onChange: (value: string) => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
const onChangeRef = useLatestRef(onChange);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
|
|
@ -153,30 +161,67 @@ export function ObsidianTextInput({
|
|||
.setPlaceholder(placeholder)
|
||||
.setValue(value)
|
||||
.onChange((nextValue) => {
|
||||
onChange(nextValue);
|
||||
onChangeRef.current(nextValue);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [onChange, placeholder, value]);
|
||||
}, [onChangeRef, placeholder, value]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianCommitTextInput({
|
||||
value,
|
||||
placeholder,
|
||||
onCommit,
|
||||
}: {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onCommit: (value: string) => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
const onCommitRef = useLatestRef(onCommit);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const text = new TextComponent(container);
|
||||
text.setPlaceholder(placeholder).setValue(value);
|
||||
const commit = () => {
|
||||
onCommitRef.current(text.inputEl.value);
|
||||
};
|
||||
return disposeDomListeners(
|
||||
listenDomEvent(text.inputEl, "blur", commit),
|
||||
listenDomEvent(text.inputEl, "keydown", (event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
commit();
|
||||
}),
|
||||
() => {
|
||||
container.empty();
|
||||
},
|
||||
);
|
||||
}, [onCommitRef, placeholder, value]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianToggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
const onChangeRef = useLatestRef(onChange);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const toggle = new ToggleComponent(container);
|
||||
toggle.setValue(checked).onChange((nextValue) => {
|
||||
onChange(nextValue);
|
||||
onChangeRef.current(nextValue);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [checked, onChange]);
|
||||
}, [checked, onChangeRef]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,9 +84,12 @@ describe("settings tab", () => {
|
|||
const tab = newSettingsTab({ saveSettings });
|
||||
|
||||
tab.display();
|
||||
expect(inputForSetting(tab, "Codex executable")?.getAttribute("aria-label")).toBeNull();
|
||||
const codexPath = inputForSetting(tab, "Codex executable");
|
||||
expect(codexPath?.getAttribute("aria-label")).toBeNull();
|
||||
expect(codexPath?.type).toBe("text");
|
||||
const shortcut = selectForSetting(tab, "Send shortcut");
|
||||
if (!shortcut) throw new Error("Missing send shortcut dropdown");
|
||||
expect(shortcut.classList.contains("dropdown")).toBe(true);
|
||||
|
||||
shortcut.value = "mod-enter";
|
||||
shortcut.dispatchEvent(new Event("change"));
|
||||
|
|
@ -105,6 +108,7 @@ describe("settings tab", () => {
|
|||
tab.display();
|
||||
const toggle = inputForSetting(tab, "Show chat toolbar");
|
||||
if (!toggle) throw new Error("Missing toolbar visibility toggle");
|
||||
expect(toggle.parentElement?.classList.contains("checkbox-container")).toBe(true);
|
||||
|
||||
toggle.checked = false;
|
||||
toggle.dispatchEvent(new Event("change"));
|
||||
|
|
@ -122,6 +126,7 @@ describe("settings tab", () => {
|
|||
tab.display();
|
||||
const toggle = inputForSetting(tab, "Scroll thread from composer line edges");
|
||||
if (!toggle) throw new Error("Missing composer line edge scroll toggle");
|
||||
expect(toggle.parentElement?.classList.contains("checkbox-container")).toBe(true);
|
||||
|
||||
toggle.checked = true;
|
||||
toggle.dispatchEvent(new Event("change"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue