feat(manager): wildcard.max_ref_depth setting (1-32, default 8)

This commit is contained in:
DumiFlex
2026-04-28 07:41:09 +03:00
parent 7f6f5b7192
commit bc1d21e8aa
5 changed files with 112 additions and 0 deletions
BIN
View File
Binary file not shown.
+47
View File
@@ -42,4 +42,51 @@ describe("Settings.vue", () => {
expect(lightChip.attributes("data-active")).toBe("true");
expect(darkChip.attributes("data-active")).toBe("false");
});
it("renders Wildcard card with max_ref_depth field", () => {
const wrap = mount(Settings);
const text = wrap.text();
expect(text).toContain("Wildcard");
expect(text).toContain("Ref recursion limit");
expect(wrap.find('[data-test="settings-wildcard-max-ref-depth"]').exists()).toBe(true);
});
it("max_ref_depth field has correct type and default value", async () => {
const wrap = mount(Settings);
const store = useUiStore();
const input = wrap.get<HTMLInputElement>('[data-test="settings-wildcard-max-ref-depth"]');
expect(input.attributes("type")).toBe("number");
expect(Number(input.element.value)).toBe(store.maxRefDepth);
});
it("changing max_ref_depth updates store and localStorage", async () => {
const wrap = mount(Settings);
const store = useUiStore();
const input = wrap.get('[data-test="settings-wildcard-max-ref-depth"]');
await input.setValue("16");
await input.trigger("blur");
expect(store.maxRefDepth).toBe(16);
expect(localStorage.getItem("wp-wildcard-max-ref-depth")).toBe("16");
});
it("max_ref_depth clamps to valid range (1-32)", async () => {
const wrap = mount(Settings);
const store = useUiStore();
const input = wrap.get('[data-test="settings-wildcard-max-ref-depth"]');
// Test clamping to min
await input.setValue("0");
await input.trigger("blur");
expect(store.maxRefDepth).toBe(1);
// Test clamping to max
await input.setValue("100");
await input.trigger("blur");
expect(store.maxRefDepth).toBe(32);
// Test valid value in range
await input.setValue("15");
await input.trigger("blur");
expect(store.maxRefDepth).toBe(15);
});
});
+9
View File
@@ -12,6 +12,9 @@ interface Props {
addon?: string;
id?: string;
ariaLabel?: string;
min?: number | string;
max?: number | string;
step?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
type: "text",
@@ -55,6 +58,9 @@ function onInput(e: Event) {
:placeholder="placeholder"
:disabled="disabled"
:aria-label="ariaLabel"
:min="min"
:max="max"
:step="step"
@input="onInput"
@blur="(e) => emit('blur', e)"
@focus="(e) => emit('focus', e)"
@@ -70,6 +76,9 @@ function onInput(e: Event) {
:placeholder="placeholder"
:disabled="disabled"
:aria-label="ariaLabel"
:min="min"
:max="max"
:step="step"
@input="onInput"
@blur="(e) => emit('blur', e)"
@focus="(e) => emit('focus', e)"
+28
View File
@@ -5,7 +5,11 @@ import { computed, ref, watch } from "vue";
export type ThemeMode = "dark" | "light" | "auto";
const STORAGE_KEY = "wp-theme-mode";
const STORAGE_KEY_MAX_REF_DEPTH = "wp-wildcard-max-ref-depth";
const FLASH_SUPPRESS_MS = 120;
const DEFAULT_MAX_REF_DEPTH = 8;
const MIN_MAX_REF_DEPTH = 1;
const MAX_MAX_REF_DEPTH = 32;
function readStoredTheme(): ThemeMode {
try {
@@ -17,6 +21,21 @@ function readStoredTheme(): ThemeMode {
return "dark";
}
function readStoredMaxRefDepth(): number {
try {
const v = localStorage.getItem(STORAGE_KEY_MAX_REF_DEPTH);
if (v !== null) {
const n = parseInt(v, 10);
if (!isNaN(n) && n >= MIN_MAX_REF_DEPTH && n <= MAX_MAX_REF_DEPTH) {
return n;
}
}
} catch {
/* localStorage unavailable */
}
return DEFAULT_MAX_REF_DEPTH;
}
function systemPrefersDark(): boolean {
return typeof window !== "undefined"
&& typeof window.matchMedia === "function"
@@ -26,6 +45,7 @@ function systemPrefersDark(): boolean {
export const useUiStore = defineStore("ui", () => {
const themeMode = ref<ThemeMode>(readStoredTheme());
const sidebarCollapsed = ref(false);
const maxRefDepth = ref<number>(readStoredMaxRefDepth());
/** Resolved theme — `"auto"` collapsed to dark/light via OS preference. */
const resolvedTheme = computed<"dark" | "light">(() =>
@@ -48,6 +68,12 @@ export const useUiStore = defineStore("ui", () => {
try { localStorage.setItem(STORAGE_KEY, mode); } catch { /* ignore */ }
}
function setMaxRefDepth(depth: number) {
const clamped = Math.max(MIN_MAX_REF_DEPTH, Math.min(MAX_MAX_REF_DEPTH, Math.floor(depth)));
maxRefDepth.value = clamped;
try { localStorage.setItem(STORAGE_KEY_MAX_REF_DEPTH, String(clamped)); } catch { /* ignore */ }
}
/** Cycle dark → light → auto → dark. */
function cycleTheme() {
const next: ThemeMode =
@@ -78,8 +104,10 @@ export const useUiStore = defineStore("ui", () => {
themeMode,
resolvedTheme,
sidebarCollapsed,
maxRefDepth,
cycleTheme,
setThemeMode,
setMaxRefDepth,
initializeTheme,
toggleSidebar,
};
+28
View File
@@ -6,7 +6,9 @@
*/
import Button from "../components/ui/Button.vue";
import Card from "../components/ui/Card.vue";
import Field from "../components/ui/Field.vue";
import Icon from "../components/ui/Icon.vue";
import Input from "../components/ui/Input.vue";
import { useUiStore, type ThemeMode } from "../stores/uiStore";
const uiStore = useUiStore();
@@ -101,6 +103,32 @@ function resetPreferences() {
@click="resetPreferences"
>Reset preferences</Button>
</Card>
<Card title="Wildcard">
<Field
label="Ref recursion limit"
hint="How deep nested @{uuid} references can resolve. Range: 1–32. Default: 8."
for="wildcard-max-ref-depth"
>
<Input
id="wildcard-max-ref-depth"
v-model.number="uiStore.maxRefDepth"
type="number"
data-test="settings-wildcard-max-ref-depth"
:aria-label="'Wildcard ref recursion limit'"
:min="1"
:max="32"
:step="1"
@blur="(e) => {
const target = e.target as HTMLInputElement;
const val = parseInt(target.value, 10);
if (!isNaN(val)) {
uiStore.setMaxRefDepth(val);
}
}"
/>
</Field>
</Card>
</div>
</template>