Refine settings helper rows

This commit is contained in:
murashit 2026-06-20 10:57:38 +09:00
parent dad4f9cb5b
commit d9d0a008a6
5 changed files with 181 additions and 97 deletions

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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} />;
}

View file

@ -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} />;
}

View file

@ -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;
}