mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 17:30:31 +00:00
Refine settings helper rows
This commit is contained in:
parent
dad4f9cb5b
commit
d9d0a008a6
5 changed files with 181 additions and 97 deletions
|
|
@ -59,25 +59,15 @@ function ModelEffortSetting({
|
|||
onChange={(value) => {
|
||||
onModelChange(value === CODEX_DEFAULT_VALUE ? null : value);
|
||||
}}
|
||||
>
|
||||
{modelSelectOptions(models, modelValue).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</SelectControl>
|
||||
options={modelSelectOptions(models, modelValue)}
|
||||
/>
|
||||
<SelectControl
|
||||
value={effortValue ?? CODEX_DEFAULT_VALUE}
|
||||
onChange={(value) => {
|
||||
onEffortChange(value === CODEX_DEFAULT_VALUE ? null : value);
|
||||
}}
|
||||
>
|
||||
{reasoningEffortSelectOptions(efforts, effortValue).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</SelectControl>
|
||||
options={reasoningEffortSelectOptions(efforts, effortValue)}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { HookItem } from "../domain/catalog/metadata";
|
||||
import { ObsidianButton } from "../shared/ui/components";
|
||||
import type { HookSectionState } from "./section-state";
|
||||
import { SettingRow, SettingsHeading } from "./setting-components";
|
||||
|
||||
|
|
@ -53,24 +54,20 @@ function HookRow({ hook, state }: { hook: HookItem; state: HookSectionState }):
|
|||
desc={`${hook.eventName} · ${hook.matcher ?? "(no matcher)"} · ${hook.trustStatus} · ${hook.enabled ? "enabled" : "disabled"}`}
|
||||
extraInfo={<div className="codex-panel-settings__hook-hash">{hook.currentHash}</div>}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<ObsidianButton
|
||||
text="Trust"
|
||||
disabled={state.loading || !canTrust}
|
||||
onClick={() => {
|
||||
state.onTrust(hook);
|
||||
}}
|
||||
>
|
||||
Trust
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
/>
|
||||
<ObsidianButton
|
||||
text={hook.enabled ? "Disable" : "Enable"}
|
||||
disabled={state.loading || hook.isManaged}
|
||||
onClick={() => {
|
||||
state.onToggleEnabled(hook, !hook.enabled);
|
||||
}}
|
||||
>
|
||||
{hook.enabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import type { ComponentChild as UiNode, TargetedMouseEvent, TargetedPointerEvent } from "preact";
|
||||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import { IconButton } from "../shared/ui/components";
|
||||
import {
|
||||
ObsidianDropdown,
|
||||
type ObsidianDropdownOption,
|
||||
ObsidianExtraButton,
|
||||
ObsidianTextInput,
|
||||
ObsidianToggle,
|
||||
} from "../shared/ui/components";
|
||||
|
||||
export type SelectControlOption = ObsidianDropdownOption;
|
||||
|
||||
export function SettingsHeading({ name, desc, dynamic = false }: { name: string; desc?: string; dynamic?: boolean }): UiNode {
|
||||
return (
|
||||
|
|
@ -8,10 +16,8 @@ export function SettingsHeading({ name, desc, dynamic = false }: { name: string;
|
|||
className={`${dynamic ? "codex-panel-settings__dynamic-section-heading" : "codex-panel-settings__section-heading"} setting-item setting-item-heading`}
|
||||
>
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-description">
|
||||
<div className="setting-item-name">{name}</div>
|
||||
{desc ?? null}
|
||||
</div>
|
||||
<div className="setting-item-name">{name}</div>
|
||||
{desc ? <div className="setting-item-description">{desc}</div> : null}
|
||||
</div>
|
||||
<div className="setting-item-control" />
|
||||
</div>
|
||||
|
|
@ -34,11 +40,9 @@ export function SettingRow({
|
|||
return (
|
||||
<div className={`setting-item ${className}`.trim()}>
|
||||
<div className="setting-item-info">
|
||||
<div className="setting-item-description">
|
||||
<div className="setting-item-name">{name}</div>
|
||||
{desc}
|
||||
{extraInfo}
|
||||
</div>
|
||||
<div className="setting-item-name">{name}</div>
|
||||
<div className="setting-item-description">{desc}</div>
|
||||
{extraInfo}
|
||||
</div>
|
||||
<div className="setting-item-control">{children}</div>
|
||||
</div>
|
||||
|
|
@ -56,43 +60,19 @@ export function SettingsIconButton({
|
|||
className: string;
|
||||
onClick: () => void;
|
||||
}): UiNode {
|
||||
return (
|
||||
<IconButton
|
||||
icon={icon}
|
||||
label={label}
|
||||
className={`clickable-icon extra-setting-button ${className}`}
|
||||
onPointerDown={(event: TargetedPointerEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event: TargetedMouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <ObsidianExtraButton icon={icon} label={label} className={className} onClick={onClick} />;
|
||||
}
|
||||
|
||||
export function SelectControl({
|
||||
value,
|
||||
onChange,
|
||||
children,
|
||||
options,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
children: UiNode;
|
||||
options: readonly SelectControlOption[];
|
||||
}): UiNode {
|
||||
return (
|
||||
<select
|
||||
className="dropdown"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
return <ObsidianDropdown value={value} options={options} onChange={onChange} />;
|
||||
}
|
||||
|
||||
export function TextControl({
|
||||
|
|
@ -104,34 +84,9 @@ export function TextControl({
|
|||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
}): UiNode {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
onChange(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <ObsidianTextInput value={value} placeholder={placeholder} onChange={onChange} />;
|
||||
}
|
||||
|
||||
export function ToggleControl({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }): UiNode {
|
||||
return (
|
||||
<div
|
||||
className={`checkbox-container ${checked ? "is-enabled" : ""}`}
|
||||
onClick={(event) => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
onChange(!checked);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(event) => {
|
||||
onChange(event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return <ObsidianToggle checked={checked} onChange={onChange} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { setIcon } from "obsidian";
|
||||
import { ButtonComponent, DropdownComponent, ExtraButtonComponent, TextComponent, ToggleComponent, setIcon } from "obsidian";
|
||||
import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact";
|
||||
import { useLayoutEffect, useRef } from "preact/hooks";
|
||||
|
||||
|
|
@ -7,6 +7,11 @@ interface ObsidianIconProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
export interface ObsidianDropdownOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function ObsidianIcon({ icon, className }: ObsidianIconProps): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
|
|
@ -54,3 +59,136 @@ export function IconButton({ icon, label, buttonRef, className, children, ...pro
|
|||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObsidianDropdown({
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
options: readonly ObsidianDropdownOption[];
|
||||
onChange: (value: string) => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const dropdown = new DropdownComponent(container);
|
||||
for (const option of options) {
|
||||
dropdown.addOption(option.value, option.label);
|
||||
}
|
||||
dropdown.setValue(value).onChange((selected) => {
|
||||
onChange(selected);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [onChange, options, value]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianTextInput({
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const text = new TextComponent(container);
|
||||
text
|
||||
.setPlaceholder(placeholder)
|
||||
.setValue(value)
|
||||
.onChange((nextValue) => {
|
||||
onChange(nextValue);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [onChange, placeholder, value]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianToggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const toggle = new ToggleComponent(container);
|
||||
toggle.setValue(checked).onChange((nextValue) => {
|
||||
onChange(nextValue);
|
||||
});
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [checked, onChange]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianExtraButton({
|
||||
icon,
|
||||
label,
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
}): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const button = new ExtraButtonComponent(container).setIcon(icon).setTooltip(label).onClick(onClick);
|
||||
button.extraSettingsEl.ariaLabel = label;
|
||||
const stopPointerDown = (event: PointerEvent): void => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
button.extraSettingsEl.addEventListener("pointerdown", stopPointerDown);
|
||||
if (className) {
|
||||
for (const classPart of className.split(" ").filter(Boolean)) {
|
||||
button.extraSettingsEl.addClass(classPart);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
button.extraSettingsEl.removeEventListener("pointerdown", stopPointerDown);
|
||||
container.empty();
|
||||
};
|
||||
}, [className, icon, label, onClick]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
export function ObsidianButton({ text, disabled, onClick }: { text: string; disabled?: boolean; onClick: () => void }): UiNode {
|
||||
const ref = useRef<HTMLSpanElement | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const container = ref.current;
|
||||
if (!container) return;
|
||||
container.empty();
|
||||
const button = new ButtonComponent(container)
|
||||
.setButtonText(text)
|
||||
.setDisabled(disabled ?? false)
|
||||
.onClick(() => {
|
||||
onClick();
|
||||
});
|
||||
button.buttonEl.type = "button";
|
||||
return () => {
|
||||
container.empty();
|
||||
};
|
||||
}, [disabled, onClick, text]);
|
||||
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export class Setting {
|
|||
}
|
||||
}
|
||||
|
||||
class ButtonComponent {
|
||||
export class ButtonComponent {
|
||||
readonly buttonEl: HTMLButtonElement;
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
|
|
@ -329,7 +329,7 @@ class ButtonComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class ExtraButtonComponent {
|
||||
export class ExtraButtonComponent {
|
||||
readonly extraSettingsEl: HTMLButtonElement;
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
|
|
@ -357,11 +357,12 @@ class ExtraButtonComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class DropdownComponent {
|
||||
export class DropdownComponent {
|
||||
readonly selectEl: HTMLSelectElement;
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
this.selectEl = parent.createEl("select");
|
||||
this.selectEl.addClass("dropdown");
|
||||
}
|
||||
|
||||
addOption(value: string, label: string): this {
|
||||
|
|
@ -385,7 +386,7 @@ class DropdownComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class TextComponent {
|
||||
export class TextComponent {
|
||||
readonly inputEl: HTMLInputElement;
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
|
|
@ -410,15 +411,18 @@ class TextComponent {
|
|||
}
|
||||
}
|
||||
|
||||
class ToggleComponent {
|
||||
export class ToggleComponent {
|
||||
readonly toggleEl: HTMLInputElement;
|
||||
private readonly containerEl: HTMLDivElement;
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
this.toggleEl = parent.createEl("input", { attr: { type: "checkbox" } });
|
||||
this.containerEl = parent.createDiv({ cls: "checkbox-container" });
|
||||
this.toggleEl = this.containerEl.createEl("input", { attr: { type: "checkbox" } });
|
||||
}
|
||||
|
||||
setValue(value: boolean): this {
|
||||
this.toggleEl.checked = value;
|
||||
this.containerEl.classList.toggle("is-enabled", value);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue