Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77fc6de807 |
+1
-1
@@ -35,7 +35,7 @@
|
||||
"announce": "node scripts/discord-announce.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dumiflex/wp-kit": "^0.17.0",
|
||||
"@dumiflex/wp-kit": "^0.18.0",
|
||||
"pinia": "^2.2.6",
|
||||
"primeicons": "^8.0.2",
|
||||
"vue": "^3.5.44",
|
||||
|
||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@dumiflex/wp-kit':
|
||||
specifier: ^0.17.0
|
||||
version: 0.17.0(primeicons@8.0.2)(vue@3.5.44(typescript@5.9.3))
|
||||
specifier: ^0.18.0
|
||||
version: 0.18.0(primeicons@8.0.2)(vue@3.5.44(typescript@5.9.3))
|
||||
pinia:
|
||||
specifier: ^2.2.6
|
||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.44(typescript@5.9.3))
|
||||
@@ -287,8 +287,8 @@ packages:
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@dumiflex/wp-kit@0.17.0':
|
||||
resolution: {integrity: sha512-7DS9FWvdS3H3MqZi48dl5+ry/7dcVCeUckUvrwqba/VNCNd+SsJ8z2ioRWGGRKMZSA912aYQBLEMkcYbXNLIrA==}
|
||||
'@dumiflex/wp-kit@0.18.0':
|
||||
resolution: {integrity: sha512-md2NF4dJJIUKXSpgUnXzzvwhznq6hYspyfjRC2T51ifBrqqfyFd5zNMN/K5gdNK6eepO74ugK0Dgt+iFTBPEHA==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
primeicons: ^7.0.0 || ^8.0.0
|
||||
@@ -2436,7 +2436,7 @@ snapshots:
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@dumiflex/wp-kit@0.17.0(primeicons@8.0.2)(vue@3.5.44(typescript@5.9.3))':
|
||||
'@dumiflex/wp-kit@0.18.0(primeicons@8.0.2)(vue@3.5.44(typescript@5.9.3))':
|
||||
dependencies:
|
||||
primeicons: 8.0.2
|
||||
vue: 3.5.44(typescript@5.9.3)
|
||||
|
||||
@@ -13,7 +13,7 @@ import { computed, nextTick, ref, watch } from "vue";
|
||||
import WpSelect from "@/kit/WpSelect.vue";
|
||||
import { api } from "../../manager/api/client";
|
||||
import type { TemplateRow } from "../../manager/api/types";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Skeleton from "@/kit/Skeleton.vue";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import Chip from "@/kit/Chip.vue";
|
||||
@@ -128,19 +128,17 @@ function pick(row: TemplateRow) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="open" labelledby="wp-ltm-title" @close="emit('close')">
|
||||
<div class="wp-ltm" @click.stop>
|
||||
<header class="wp-ltm__head">
|
||||
<div class="wp-ltm__head-text">
|
||||
<div class="wp-ltm__eyebrow">Load template from library</div>
|
||||
<div id="wp-ltm-title" class="wp-ltm__title">
|
||||
Browse Templates
|
||||
<span class="wp-ltm__title-detail">· {{ rows.length }} {{ rows.length === 1 ? "template" : "templates" }}</span>
|
||||
</div>
|
||||
<Modal :open="open" size="lg" flush accent="brand" icon="folder-open" @update:open="(v: boolean) => { if (!v) emit('close'); }">
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-ltm__head-text">
|
||||
<div class="wp-ltm__eyebrow">Load template from library</div>
|
||||
<div :id="titleId" class="wp-ltm__title">
|
||||
Browse Templates
|
||||
<span class="wp-ltm__title-detail">· {{ rows.length }} {{ rows.length === 1 ? "template" : "templates" }}</span>
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="load-tpl-close" @click="emit('close')" />
|
||||
</header>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<div class="wp-ltm">
|
||||
<div class="wp-ltm__toolbar">
|
||||
<SearchInput
|
||||
ref="searchInput"
|
||||
@@ -269,49 +267,26 @@ function pick(row: TemplateRow) {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer class="wp-ltm__foot">
|
||||
<span class="wp-ltm__hint">Click a template to load it into the assembler.</span>
|
||||
<span class="wp-ltm__foot-spacer" />
|
||||
<Button size="sm" data-test="load-tpl-cancel" @click="emit('close')">Cancel</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<span class="wp-ltm__hint">Click a template to load it into the assembler.</span>
|
||||
<span class="wp-ltm__foot-spacer" />
|
||||
<Button size="sm" data-test="load-tpl-cancel" @click="emit('close')">Cancel</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-ltm {
|
||||
background: var(--wp-bg-2);
|
||||
border: 1px solid var(--wp-border-strong, var(--wp-border));
|
||||
border-radius: var(--wp-radius-lg, 12px);
|
||||
box-shadow: var(--wp-shadow-lg, 0 20px 60px rgba(0, 0, 0, 0.55));
|
||||
width: 640px;
|
||||
max-width: calc(100vw - 32px);
|
||||
height: 560px;
|
||||
max-height: calc(100vh - 32px);
|
||||
height: 480px;
|
||||
max-height: calc(100vh - 160px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--wp-text);
|
||||
overflow: hidden;
|
||||
font-family: var(--wp-font-sans, sans-serif);
|
||||
}
|
||||
|
||||
/* Header — brand wash mirrors ModulePickerModal. */
|
||||
.wp-ltm__head {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
background: var(--wp-brand-gradient, transparent);
|
||||
position: relative;
|
||||
}
|
||||
.wp-ltm__head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--wp-bg) 85%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-ltm__head > * { position: relative; z-index: 1; }
|
||||
.wp-ltm__head-text { flex: 1; min-width: 0; }
|
||||
.wp-ltm__head-text { flex: 1 1 auto; min-width: 0; }
|
||||
.wp-ltm__eyebrow {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
@@ -453,12 +428,6 @@ function pick(row: TemplateRow) {
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.wp-ltm__foot {
|
||||
height: 44px; padding: 0 16px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg-1);
|
||||
color: var(--wp-text-dim); font-size: 11px;
|
||||
}
|
||||
.wp-ltm__foot-spacer { flex: 1; }
|
||||
.wp-ltm__hint { align-self: center; color: var(--wp-text-dim); font-size: 11px; }
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { api } from "../../manager/api/client";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Textarea from "@/kit/Textarea.vue";
|
||||
import Input from "@/kit/Input.vue";
|
||||
import WpSelect from "@/kit/WpSelect.vue";
|
||||
@@ -100,12 +100,8 @@ async function submit(mode: "update" | "new") {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="open" labelledby="wp-stm-title" @close="emit('close')">
|
||||
<div class="wp-stm" @click.stop>
|
||||
<header class="wp-stm__head">
|
||||
<h3 id="wp-stm-title" class="wp-stm__title">Save template</h3>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="save-tpl-close" @click="emit('close')" />
|
||||
</header>
|
||||
<Modal :open="open" title="Save template" size="md" flush accent="brand" icon="save" @update:open="(v: boolean) => { if (!v) emit('close'); }">
|
||||
<div class="wp-stm">
|
||||
|
||||
<div class="wp-stm__body">
|
||||
<div class="wp-stm-field">
|
||||
@@ -182,60 +178,30 @@ async function submit(mode: "update" | "new") {
|
||||
<p v-if="errorMsg" class="wp-stm-error" data-test="save-tpl-error">{{ errorMsg }}</p>
|
||||
</div>
|
||||
|
||||
<footer class="wp-stm__foot">
|
||||
<Button size="sm" data-test="save-tpl-cancel" @click="emit('close')">Cancel</Button>
|
||||
<span class="wp-stm-spacer" />
|
||||
<Button
|
||||
v-if="updateTargetId"
|
||||
size="sm"
|
||||
:disabled="!canSubmit"
|
||||
data-test="save-tpl-save-new"
|
||||
@click="submit('new')"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:disabled="!canSubmit"
|
||||
data-test="save-tpl-submit"
|
||||
@click="submit(updateTargetId ? 'update' : 'new')"
|
||||
>{{ updateTargetId ? "Update existing" : "Save" }}</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="sm" data-test="save-tpl-cancel" @click="emit('close')">Cancel</Button>
|
||||
<span class="wp-stm-spacer" />
|
||||
<Button
|
||||
v-if="updateTargetId"
|
||||
size="sm"
|
||||
:disabled="!canSubmit"
|
||||
data-test="save-tpl-save-new"
|
||||
@click="submit('new')"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:disabled="!canSubmit"
|
||||
data-test="save-tpl-submit"
|
||||
@click="submit(updateTargetId ? 'update' : 'new')"
|
||||
>{{ updateTargetId ? "Update existing" : "Save" }}</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-stm {
|
||||
width: min(560px, 100%);
|
||||
max-height: calc(100vh - 40px);
|
||||
background: var(--wp-bg-1);
|
||||
border: 1px solid var(--wp-border-strong, var(--wp-border));
|
||||
border-radius: var(--wp-radius-lg, 12px);
|
||||
box-shadow: var(--wp-shadow-lg, 0 20px 60px rgba(0, 0, 0, 0.55));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
color: var(--wp-text);
|
||||
font-family: var(--wp-font-sans, sans-serif);
|
||||
}
|
||||
.wp-stm__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
background: var(--wp-brand-gradient, transparent);
|
||||
position: relative;
|
||||
}
|
||||
.wp-stm__head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--wp-bg) 85%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-stm__head > * { position: relative; z-index: 1; }
|
||||
.wp-stm__title { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.wp-stm { display: flex; flex-direction: column; color: var(--wp-text); font-family: var(--wp-font-sans, sans-serif); }
|
||||
|
||||
.wp-stm__body {
|
||||
flex: 1;
|
||||
@@ -280,13 +246,5 @@ async function submit(mode: "update" | "new") {
|
||||
color: var(--wp-danger);
|
||||
}
|
||||
|
||||
.wp-stm__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 16px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg-1);
|
||||
}
|
||||
.wp-stm-spacer { flex: 1; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Body-teleported modal for the cleaner's blocklist. Mode toggle:
|
||||
* Kit Modal for the cleaner's blocklist. Mode toggle:
|
||||
* - "list" — comma- or newline-separated, exact tag match (case-insensitive)
|
||||
* - "regex" — one regex per line, case-insensitive
|
||||
*
|
||||
@@ -8,7 +8,7 @@
|
||||
* Cancel discards. Parent gates visibility via `visible`.
|
||||
*/
|
||||
import { ref, watch, computed } from "vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Textarea from "@/kit/Textarea.vue";
|
||||
import type { BlocklistKind } from "./types";
|
||||
import Button from "@/kit/Button.vue";
|
||||
@@ -60,11 +60,8 @@ function onCancel(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="visible" @close="onCancel">
|
||||
<Modal :open="visible" title="Blocklist" size="md" flush @update:open="(v: boolean) => { if (!v) onCancel(); }">
|
||||
<div class="wp-blocklist">
|
||||
<header class="wp-blocklist__head">
|
||||
<span class="wp-blocklist__title">Blocklist</span>
|
||||
</header>
|
||||
<div class="wp-blocklist__mode-row">
|
||||
<span class="wp-blocklist__label">MODE</span>
|
||||
<WpSegmented
|
||||
@@ -93,28 +90,15 @@ function onCancel(): void {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="wp-blocklist__foot">
|
||||
<Button size="sm" data-test="blocklist-cancel" @click="onCancel">Cancel</Button>
|
||||
<Button size="sm" variant="primary" data-test="blocklist-save" @click="onSave">Save</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="sm" data-test="blocklist-cancel" @click="onCancel">Cancel</Button>
|
||||
<Button size="sm" variant="primary" data-test="blocklist-save" @click="onSave">Save</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-blocklist {
|
||||
width: 480px;
|
||||
max-width: 90vw;
|
||||
background: var(--wp-bg);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius, 6px);
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-blocklist__head {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
}
|
||||
.wp-blocklist__title { font-size: 14px; font-weight: 600; }
|
||||
.wp-blocklist__mode-row {
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
@@ -151,11 +135,4 @@ function onCancel(): void {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.wp-blocklist__foot {
|
||||
padding: 10px 16px 12px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
* run, so the grid can be checked before anything is queued.
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import SweepPanel from "./SweepPanel.vue";
|
||||
import { liveAxes, sweepTotal, type SweepConfig } from "./types";
|
||||
import type { SweepCandidate } from "./sweep-candidates";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import Icon from "@/kit/Icon.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -63,17 +62,14 @@ const preview = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="true" @close="emit('close')">
|
||||
<div class="wp-swm" data-test="sweep-modal">
|
||||
<div class="wp-swm__head">
|
||||
<span class="wp-swm__head-icon"><Icon name="th-large" :size="15" /></span>
|
||||
<div class="wp-swm__title-block">
|
||||
<div class="wp-swm__title-row"><span class="wp-swm__name">WP Context Loop</span><span class="wp-swm__chip">Sweep</span></div>
|
||||
<div class="wp-swm__sub">Run every combination of the wildcards you pick, one frame each.</div>
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="sweep-modal-close" @click="emit('close')" />
|
||||
<Modal :open="true" size="md" flush accent="var(--wp-accent)" icon="th-large" @update:open="(v: boolean) => { if (!v) emit('close'); }">
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-swm__title-block">
|
||||
<div class="wp-swm__title-row"><span :id="titleId" class="wp-swm__name">WP Context Loop</span><span class="wp-swm__chip">Sweep</span></div>
|
||||
<div class="wp-swm__sub">Run every combination of the wildcards you pick, one frame each.</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<div class="wp-swm" data-test="sweep-modal">
|
||||
<div class="wp-swm__body">
|
||||
<SweepPanel
|
||||
:model-value="modelValue"
|
||||
@@ -94,23 +90,21 @@ const preview = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-swm__foot">
|
||||
<span class="wp-swm__foot-hint">Changes save as you make them.</span>
|
||||
<span class="wp-swm__spacer" />
|
||||
<Button size="sm" variant="primary" data-test="sweep-modal-done" @click="emit('close')">Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<span class="wp-swm__foot-hint">Changes save as you make them.</span>
|
||||
<span class="wp-swm__spacer" />
|
||||
<Button size="sm" variant="primary" data-test="sweep-modal-done" @click="emit('close')">Done</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-swm { width: 520px; max-width: 100%; max-height: 86vh; display: flex; flex-direction: column; background: var(--wp-bg2); border: 1px solid var(--wp-border); border-radius: min(6px, var(--wp-radius-cap, 999px)); overflow: hidden; color: var(--wp-text); font-size: 12px; box-shadow: var(--wp-shadow-lg); }
|
||||
.wp-swm__head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; flex-shrink: 0; border-bottom: 1px solid var(--wp-border); background: color-mix(in srgb, var(--wp-accent) 9%, var(--wp-bg2)); }
|
||||
.wp-swm__head-icon { color: var(--wp-accent); width: 24px; display: flex; justify-content: center; }
|
||||
.wp-swm__title-block { flex: 1; min-width: 0; }
|
||||
.wp-swm { display: flex; flex-direction: column; min-height: 0; color: var(--wp-text); font-size: 12px; }
|
||||
.wp-swm__title-block { flex: 1 1 auto; min-width: 0; }
|
||||
.wp-swm__title-row { display: flex; align-items: center; gap: 8px; }
|
||||
.wp-swm__name { font: 600 13px var(--wp-font-sans); }
|
||||
.wp-swm__chip { font: 600 9px var(--wp-font-sans); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: min(2px, var(--wp-radius-cap, 999px)); background: color-mix(in srgb, var(--wp-accent) 22%, transparent); color: var(--wp-accent); }
|
||||
.wp-swm__chip { font: 600 9px var(--wp-font-sans); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: min(2px, var(--wp-radius-cap, 999px)); background: color-mix(in srgb, var(--wp-modal-accent) 22%, transparent); color: var(--wp-modal-accent); }
|
||||
.wp-swm__sub { font: 10.5px var(--wp-font-sans); color: var(--wp-text-dim, var(--wp-text3)); margin-top: 3px; }
|
||||
|
||||
.wp-swm__body { overflow-y: auto; padding: 12px 14px; flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 12px; font-size: 11px; }
|
||||
@@ -122,7 +116,6 @@ const preview = computed(() => {
|
||||
.wp-swm__frame-vals { color: var(--wp-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.wp-swm__frame-more { padding: 4px 0 0 42px; font: 10.5px var(--wp-font-sans); color: var(--wp-text-dim, var(--wp-text3)); }
|
||||
|
||||
.wp-swm__foot { display: flex; align-items: center; gap: 12px; padding: 10px 14px; flex-shrink: 0; background: var(--wp-bg3); border-top: 1px solid var(--wp-border); }
|
||||
.wp-swm__foot-hint { font: 10px var(--wp-font-sans); color: var(--wp-text-dim, var(--wp-text3)); }
|
||||
.wp-swm__foot-hint { align-self: center; font: 10px var(--wp-font-sans); color: var(--wp-text-dim, var(--wp-text3)); }
|
||||
.wp-swm__spacer { flex: 1; }
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Kbd from "@/kit/Kbd.vue";
|
||||
import SearchInput from "@/kit/SearchInput.vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import ConfirmModal from "@/kit/ConfirmModal.vue";
|
||||
import { api } from "../../manager/api/client";
|
||||
import type { BundleRow } from "../../manager/api/types";
|
||||
@@ -178,19 +178,17 @@ const alreadyAddedSet = computed(() => new Set(props.alreadyAddedIds ?? []));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="visible" label="Add bundle from library" @close="emit('close')" @keydown="onKeydown">
|
||||
<div class="wp-bp">
|
||||
<header class="wp-bp__head">
|
||||
<div class="wp-bp__head-text">
|
||||
<div class="wp-bp__eyebrow">Add bundle from library</div>
|
||||
<div class="wp-bp__title">
|
||||
Browse Bundles
|
||||
<span class="wp-bp__title-detail">· {{ filtered.length }} {{ filtered.length === 1 ? "bundle" : "bundles" }}</span>
|
||||
</div>
|
||||
<Modal :open="visible" size="lg" flush accent="brand" icon="box" @update:open="(v: boolean) => { if (!v) emit('close'); }" @keydown="onKeydown">
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-bp__head-text">
|
||||
<div class="wp-bp__eyebrow">Add bundle from library</div>
|
||||
<div :id="titleId" class="wp-bp__title">
|
||||
Browse Bundles
|
||||
<span class="wp-bp__title-detail">· {{ filtered.length }} {{ filtered.length === 1 ? "bundle" : "bundles" }}</span>
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" @click="emit('close')" />
|
||||
</header>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<div class="wp-bp">
|
||||
<div class="wp-bp__toolbar">
|
||||
<SearchInput
|
||||
ref="searchInput"
|
||||
@@ -263,32 +261,32 @@ const alreadyAddedSet = computed(() => new Set(props.alreadyAddedIds ?? []));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-bp__foot">
|
||||
<div class="wp-bp__hints">
|
||||
<Kbd keys="↑" size="sm" />
|
||||
<Kbd keys="↓" size="sm" />
|
||||
<span class="wp-bp__hint-label">nav</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="Enter" size="sm" />
|
||||
<span class="wp-bp__hint-label">insert</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="/" size="sm" />
|
||||
<span class="wp-bp__hint-label">search</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="Esc" size="sm" />
|
||||
<span class="wp-bp__hint-label">cancel</span>
|
||||
</div>
|
||||
<Button size="sm" @click="emit('close')">Cancel</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="pi-plus"
|
||||
data-testid="bundle-picker-create"
|
||||
@click="emit('create')"
|
||||
>Create Bundle</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<div class="wp-bp__hints">
|
||||
<Kbd keys="↑" size="sm" />
|
||||
<Kbd keys="↓" size="sm" />
|
||||
<span class="wp-bp__hint-label">nav</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="Enter" size="sm" />
|
||||
<span class="wp-bp__hint-label">insert</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="/" size="sm" />
|
||||
<span class="wp-bp__hint-label">search</span>
|
||||
<span class="wp-bp__hint-sep">·</span>
|
||||
<Kbd keys="Esc" size="sm" />
|
||||
<span class="wp-bp__hint-label">cancel</span>
|
||||
</div>
|
||||
<Button size="sm" @click="emit('close')">Cancel</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="pi-plus"
|
||||
data-testid="bundle-picker-create"
|
||||
@click="emit('create')"
|
||||
>Create Bundle</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<ConfirmModal
|
||||
:open="pendingReinsert !== null"
|
||||
:title="confirmTitle"
|
||||
@@ -305,37 +303,12 @@ const alreadyAddedSet = computed(() => new Set(props.alreadyAddedIds ?? []));
|
||||
.wp-bp__skel-list { padding: 6px 16px; }
|
||||
|
||||
.wp-bp {
|
||||
background: var(--wp-bg-deep, var(--wp-bg));
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: min(8px, var(--wp-radius-cap, 999px));
|
||||
width: 720px;
|
||||
max-width: 92vw;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--wp-shadow-lg, 0 20px 60px rgba(0, 0, 0, 0.5));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Header — brand gradient + dark wash overlay, matches edit modals. */
|
||||
.wp-bp__head {
|
||||
background: var(--wp-brand-gradient);
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
position: relative;
|
||||
}
|
||||
.wp-bp__head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
/* Theme-aware wash so the brand gradient reads correctly in both palettes. */
|
||||
background: color-mix(in srgb, var(--wp-bg) 85%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-bp__head > * { position: relative; z-index: 1; }
|
||||
.wp-bp__head-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||
.wp-bp__head-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
||||
.wp-bp__eyebrow {
|
||||
font: 500 11px/1.2 var(--wp-font-sans);
|
||||
color: var(--wp-text-dim, var(--wp-text3));
|
||||
@@ -461,14 +434,6 @@ const alreadyAddedSet = computed(() => new Set(props.alreadyAddedIds ?? []));
|
||||
|
||||
/* Footer — keyboard hints inline with Cancel + Create. Matches the
|
||||
* module picker pattern: the keys are the kit's Kbd. */
|
||||
.wp-bp__foot {
|
||||
border-top: 1px solid var(--wp-border);
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--wp-bg-deep, var(--wp-bg));
|
||||
}
|
||||
.wp-bp__hints {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<template>
|
||||
<ModalShell :visible="visible" @close="$emit('close')">
|
||||
<div ref="rootEl" class="wp-picker" tabindex="-1">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="wp-picker__head">
|
||||
<div class="wp-picker__head-text">
|
||||
<div class="wp-picker__eyebrow">
|
||||
Add modules from library
|
||||
</div>
|
||||
<div class="wp-picker__title">
|
||||
Browse Library
|
||||
<span class="wp-picker__title-detail">
|
||||
· {{ modules.length }} {{ modules.length === 1 ? "module" : "modules" }}
|
||||
</span>
|
||||
</div>
|
||||
<Modal :open="visible" size="lg" flush accent="brand" icon="th-large" @update:open="(v: boolean) => { if (!v) $emit('close'); }">
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-picker__head-text">
|
||||
<div class="wp-picker__eyebrow">
|
||||
Add modules from library
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" @click="$emit('close')" />
|
||||
</header>
|
||||
<div :id="titleId" class="wp-picker__title">
|
||||
Browse Library
|
||||
<span class="wp-picker__title-detail">
|
||||
· {{ modules.length }} {{ modules.length === 1 ? "module" : "modules" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="rootEl" class="wp-picker" tabindex="-1">
|
||||
|
||||
<!-- Toolbar: search + kind tabs + compact filter buttons.
|
||||
Filters are inline on the toolbar (no dedicated row) so
|
||||
@@ -309,7 +306,7 @@
|
||||
>Add {{ selected.size }} {{ selected.size === 1 ? "module" : "modules" }}</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -332,7 +329,7 @@
|
||||
import Kbd from "@/kit/Kbd.vue";
|
||||
import SearchInput from "@/kit/SearchInput.vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Skeleton from "@/kit/Skeleton.vue";
|
||||
import WpSelect from "@/kit/WpSelect.vue";
|
||||
import { kindIcon } from "../shared/kind-icons";
|
||||
@@ -970,41 +967,17 @@ onBeforeUnmount(detachCaptureListeners);
|
||||
.wp-picker__skel-rows { padding: 0 10px; }
|
||||
|
||||
.wp-picker {
|
||||
background: var(--wp-bg-2);
|
||||
border: 1px solid var(--wp-border-strong);
|
||||
border-radius: var(--wp-radius-lg);
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
width: 780px;
|
||||
max-width: calc(100vw - 32px);
|
||||
height: 600px;
|
||||
max-height: calc(100vh - 32px);
|
||||
height: 560px;
|
||||
max-height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--wp-text);
|
||||
overflow: hidden;
|
||||
font-family: var(--wp-font);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ── Header — brand gradient + dark wash overlay. Mirrors BundlePicker
|
||||
* so both pickers read as part of the same family. */
|
||||
.wp-picker__head {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
background: var(--wp-brand-gradient);
|
||||
position: relative;
|
||||
}
|
||||
.wp-picker__head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
/* Theme-aware wash so the brand gradient stays subtle in both
|
||||
* palettes — dark bg in dark mode, near-white in light mode. */
|
||||
background: color-mix(in srgb, var(--wp-bg) 85%, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-picker__head > * { position: relative; z-index: 1; }
|
||||
.wp-picker__head-text { flex: 1; min-width: 0; }
|
||||
/* ── Header text (Modal draws the brand wash). */
|
||||
.wp-picker__head-text { flex: 1 1 auto; min-width: 0; }
|
||||
.wp-picker__eyebrow {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -29,7 +29,7 @@ import { bundleContentKey } from "./bundles/bundle-fingerprint";
|
||||
import type { BundleInstance } from "../../widgets/_shared";
|
||||
import Checkbox from "@/kit/Checkbox.vue";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import { vToggleRow } from "@/kit/toggle-row";
|
||||
|
||||
interface ChildPreview {
|
||||
@@ -421,16 +421,8 @@ function pickCopyName(base: string, taken: ReadonlySet<string>): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="!!(open && bundle)" labelledby="wp-pbtl-title" @close="close">
|
||||
<div
|
||||
class="wp-ptl"
|
||||
data-test="pbtl"
|
||||
>
|
||||
<header class="wp-ptl__head">
|
||||
<h3 id="wp-pbtl-title" class="wp-ptl__title">Push bundle to library</h3>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="pbtl-close" @click="close" />
|
||||
</header>
|
||||
|
||||
<Modal :open="!!(open && bundle)" title="Push bundle to library" size="md" flush @update:open="(v: boolean) => { if (!v) close(); }">
|
||||
<div class="wp-ptl" data-test="pbtl">
|
||||
<div class="wp-ptl__body">
|
||||
<div class="wp-ptl-field">
|
||||
<label class="wp-ptl-field__label" for="wp-pbtl-name">Name</label>
|
||||
@@ -605,46 +597,30 @@ function pickCopyName(base: string, taken: ReadonlySet<string>): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="wp-ptl__foot">
|
||||
<Button size="sm" data-test="pbtl-cancel" :disabled="busy" @click="close">Cancel</Button>
|
||||
<span class="wp-ptl-spacer" />
|
||||
<Button
|
||||
size="sm"
|
||||
data-test="pbtl-save-new"
|
||||
:disabled="busy"
|
||||
@click="doSaveAsNew"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
data-test="pbtl-update"
|
||||
:disabled="busy || !canUpdateExisting"
|
||||
:hint="!isLibraryTracked ? 'No existing library entry to update' : isLibraryMissing ? 'Library entry has been deleted — save as new entry instead' : undefined"
|
||||
@click="doUpdate"
|
||||
>Update existing</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="sm" data-test="pbtl-cancel" :disabled="busy" @click="close">Cancel</Button>
|
||||
<span class="wp-ptl-spacer" />
|
||||
<Button
|
||||
size="sm"
|
||||
data-test="pbtl-save-new"
|
||||
:disabled="busy"
|
||||
@click="doSaveAsNew"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
data-test="pbtl-update"
|
||||
:disabled="busy || !canUpdateExisting"
|
||||
:hint="!isLibraryTracked ? 'No existing library entry to update' : isLibraryMissing ? 'Library entry has been deleted — save as new entry instead' : undefined"
|
||||
@click="doUpdate"
|
||||
>Update existing</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-ptl {
|
||||
width: min(620px, 100%);
|
||||
max-height: calc(100vh - 32px);
|
||||
background: var(--wp-bg-1);
|
||||
border: 1px solid var(--wp-border-strong, var(--wp-border));
|
||||
border-radius: var(--wp-radius-lg, 10px);
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-ptl__head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
}
|
||||
.wp-ptl__title { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.wp-ptl { color: var(--wp-text); }
|
||||
.wp-ptl__body {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
padding: 14px;
|
||||
@@ -801,11 +777,5 @@ function pickCopyName(base: string, taken: ReadonlySet<string>): string {
|
||||
color: var(--wp-danger);
|
||||
font-size: 12px;
|
||||
}
|
||||
.wp-ptl__foot {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg-1);
|
||||
}
|
||||
.wp-ptl-spacer { flex: 1; }
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,7 @@ import type { ModuleEntry } from "../../widgets/_shared";
|
||||
import Checkbox from "@/kit/Checkbox.vue";
|
||||
import LoadingMark from "@/kit/LoadingMark.vue";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import CodeBlock from "@/kit/CodeBlock.vue";
|
||||
import { vToggleRow } from "@/kit/toggle-row";
|
||||
|
||||
@@ -443,16 +443,8 @@ async function doSaveAsNew(): Promise<void> {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="!!(open && draft)" labelledby="wp-ptl-title" @close="close">
|
||||
<div
|
||||
class="wp-ptl"
|
||||
data-test="ptl"
|
||||
>
|
||||
<header class="wp-ptl__head">
|
||||
<h3 id="wp-ptl-title" class="wp-ptl__title">Push to library</h3>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="ptl-close" @click="close" />
|
||||
</header>
|
||||
|
||||
<Modal :open="!!(open && draft)" title="Push to library" size="md" flush @update:open="(v: boolean) => { if (!v) close(); }">
|
||||
<div class="wp-ptl" data-test="ptl">
|
||||
<div class="wp-ptl__body">
|
||||
<div class="wp-ptl-field">
|
||||
<label class="wp-ptl-field__label" for="wp-ptl-name">Name</label>
|
||||
@@ -603,46 +595,30 @@ async function doSaveAsNew(): Promise<void> {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="wp-ptl__foot">
|
||||
<Button size="sm" data-test="ptl-cancel" :disabled="busy" @click="close">Cancel</Button>
|
||||
<span class="wp-ptl-spacer" />
|
||||
<Button
|
||||
size="sm"
|
||||
data-test="ptl-save-new"
|
||||
:disabled="busy"
|
||||
@click="doSaveAsNew"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
data-test="ptl-update"
|
||||
:disabled="busy || !canUpdateExisting"
|
||||
:hint="!isLibraryTracked ? 'No existing library entry to update' : isLibraryMissing ? 'Library entry has been deleted — save as new entry instead' : undefined"
|
||||
@click="doUpdate"
|
||||
>Update existing</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="sm" data-test="ptl-cancel" :disabled="busy" @click="close">Cancel</Button>
|
||||
<span class="wp-ptl-spacer" />
|
||||
<Button
|
||||
size="sm"
|
||||
data-test="ptl-save-new"
|
||||
:disabled="busy"
|
||||
@click="doSaveAsNew"
|
||||
>Save as new entry</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
data-test="ptl-update"
|
||||
:disabled="busy || !canUpdateExisting"
|
||||
:hint="!isLibraryTracked ? 'No existing library entry to update' : isLibraryMissing ? 'Library entry has been deleted — save as new entry instead' : undefined"
|
||||
@click="doUpdate"
|
||||
>Update existing</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-ptl {
|
||||
width: min(620px, 100%);
|
||||
max-height: calc(100vh - 32px);
|
||||
background: var(--wp-bg-1);
|
||||
border: 1px solid var(--wp-border-strong, var(--wp-border));
|
||||
border-radius: var(--wp-radius-lg, 10px);
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-ptl__head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
}
|
||||
.wp-ptl__title { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
.wp-ptl { color: var(--wp-text); }
|
||||
.wp-ptl__body {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
padding: 14px;
|
||||
@@ -759,11 +735,5 @@ async function doSaveAsNew(): Promise<void> {
|
||||
color: var(--wp-danger);
|
||||
font-size: 12px;
|
||||
}
|
||||
.wp-ptl__foot {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg-1);
|
||||
}
|
||||
.wp-ptl-spacer { flex: 1; }
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* it: the run keeps waiting until one of the three answers is sent.
|
||||
*/
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Textarea from "@/kit/Textarea.vue";
|
||||
import WpSegmented, { type SegmentedOption } from "@/kit/WpSegmented.vue";
|
||||
import WpImageZoom from "@/kit/WpImageZoom.vue";
|
||||
@@ -263,8 +263,9 @@ const painting = ref(false);
|
||||
const pickerEl = ref<HTMLElement | null>(null);
|
||||
function keepFocusInside(): void {
|
||||
const root = pickerEl.value;
|
||||
const dialog = root?.closest<HTMLElement>(".wp-modal") ?? root;
|
||||
const at = document.activeElement;
|
||||
if (root && !(at instanceof Node && root.contains(at))) root.focus({ preventScroll: true });
|
||||
if (root && !(at instanceof Node && dialog?.contains(at))) root.focus({ preventScroll: true });
|
||||
}
|
||||
watch([mode, () => painting.value], () => { void nextTick(keepFocusInside); }, { flush: "post" });
|
||||
const tool = ref<MaskTool>("brush");
|
||||
@@ -445,6 +446,9 @@ const now = ref(Date.now());
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => { now.value = Date.now(); }, 1000);
|
||||
// Focus the picker itself, not its first control (Modal leaves focus where it
|
||||
// is when it is already inside): the keys work from there, and no tooltip pops.
|
||||
pickerEl.value?.focus({ preventScroll: true });
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) clearInterval(timer);
|
||||
@@ -546,33 +550,40 @@ const subLine = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="true" @close="onClose" @keydown="onKey">
|
||||
<div ref="pickerEl" class="wp-ifp" :class="[`is-${mode}`, { 'is-fit': isFit }]" data-test="image-filter-picker" :data-mode="mode" tabindex="-1">
|
||||
<div class="wp-ifp__head">
|
||||
<i class="pi pi-filter wp-ifp__head-icon" aria-hidden="true" />
|
||||
<div class="wp-ifp__title-block">
|
||||
<div class="wp-ifp__title-row">
|
||||
<span class="wp-ifp__name">WP Image Filter</span>
|
||||
<span class="wp-ifp__chip">Waiting</span>
|
||||
<span v-if="moreWaiting" class="wp-ifp__more" data-test="image-filter-more">+{{ moreWaiting }} more waiting</span>
|
||||
</div>
|
||||
<div class="wp-ifp__sub">
|
||||
<template v-if="multiFrame">{{ frames.length }} frames, {{ total }} images.</template>
|
||||
<template v-else>{{ total }} {{ total === 1 ? "image" : "images" }}.</template>
|
||||
{{ subLine }}
|
||||
</div>
|
||||
<Modal
|
||||
:open="true"
|
||||
size="full"
|
||||
flush
|
||||
accent="var(--wp-accent)"
|
||||
icon="filter"
|
||||
@update:open="(v: boolean) => { if (!v) onClose(); }"
|
||||
@keydown="onKey"
|
||||
>
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-ifp__title-block">
|
||||
<div class="wp-ifp__title-row">
|
||||
<span :id="titleId" class="wp-ifp__name">WP Image Filter</span>
|
||||
<span class="wp-ifp__chip">Waiting</span>
|
||||
<span v-if="moreWaiting" class="wp-ifp__more" data-test="image-filter-more">+{{ moreWaiting }} more waiting</span>
|
||||
</div>
|
||||
<div class="wp-ifp__sub">
|
||||
<template v-if="multiFrame">{{ frames.length }} frames, {{ total }} images.</template>
|
||||
<template v-else>{{ total }} {{ total === 1 ? "image" : "images" }}.</template>
|
||||
{{ subLine }}
|
||||
</div>
|
||||
<WpSegmented
|
||||
:model-value="mode"
|
||||
:options="MODE_OPTIONS"
|
||||
size="sm"
|
||||
aria-label="Mode"
|
||||
test-prefix="image-filter-mode"
|
||||
:disabled="!total"
|
||||
@update:model-value="(m) => setMode(m)"
|
||||
/>
|
||||
<Button size="sm" variant="ghost" icon="pi-minus" aria-label="Hide for now (the run keeps waiting)" data-test="image-filter-minimize" @click="emit('minimize')" />
|
||||
</div>
|
||||
<WpSegmented
|
||||
:model-value="mode"
|
||||
:options="MODE_OPTIONS"
|
||||
size="sm"
|
||||
aria-label="Mode"
|
||||
test-prefix="image-filter-mode"
|
||||
:disabled="!total"
|
||||
@update:model-value="(m) => setMode(m)"
|
||||
/>
|
||||
<Button size="sm" variant="ghost" icon="pi-minus" aria-label="Hide for now (the run keeps waiting)" data-test="image-filter-minimize" @click="emit('minimize')" />
|
||||
</template>
|
||||
<div ref="pickerEl" class="wp-ifp" :class="[`is-${mode}`, { 'is-fit': isFit }]" data-test="image-filter-picker" :data-mode="mode" tabindex="-1">
|
||||
|
||||
<!-- Grid -->
|
||||
<template v-if="mode === 'grid'">
|
||||
@@ -993,28 +1004,27 @@ const subLine = computed(() => {
|
||||
</WpImageZoom>
|
||||
</div>
|
||||
|
||||
<div class="wp-ifp__foot">
|
||||
<div class="wp-ifp__status" data-test="image-filter-status">
|
||||
<span><strong>{{ pickedList.length }}</strong> of {{ total }} picked<template v-if="multiFrame"> from {{ pickedFrames }} {{ pickedFrames === 1 ? "frame" : "frames" }}</template></span>
|
||||
<span v-if="editCount" class="wp-ifp__edits">{{ editCount }} refined</span>
|
||||
<span v-if="leftLabel" class="wp-ifp__left" :class="{ 'is-low': (left ?? 99) <= 30 }">{{ leftLabel }}</span>
|
||||
</div>
|
||||
<span class="wp-ifp__spacer" />
|
||||
<Button v-if="canSendToLoop" size="sm" :disabled="pickedFrames === 0" hint="Lock the picked frames' seeds in the Context Loop and bypass the rest, so the next run repeats only your picks" data-test="image-filter-send-loop" @click="sendToLoop">Send to Loop</Button>
|
||||
<Button size="sm" variant="danger" hint="Skip everything after this node for this run" data-test="image-filter-stop" @click="stop">Stop branch</Button>
|
||||
<Button size="sm" data-test="image-filter-keep-all" @click="keepAll">Keep all {{ total }}</Button>
|
||||
<Button size="sm" variant="primary" kbd="Enter" :disabled="pickedList.length === 0" data-test="image-filter-keep-picked" @click="keepPicked">{{ pickedList.length ? `Keep ${pickedList.length} picked` : "Keep picked" }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<div class="wp-ifp__status" data-test="image-filter-status">
|
||||
<span><strong>{{ pickedList.length }}</strong> of {{ total }} picked<template v-if="multiFrame"> from {{ pickedFrames }} {{ pickedFrames === 1 ? "frame" : "frames" }}</template></span>
|
||||
<span v-if="editCount" class="wp-ifp__edits">{{ editCount }} refined</span>
|
||||
<span v-if="leftLabel" class="wp-ifp__left" :class="{ 'is-low': (left ?? 99) <= 30 }">{{ leftLabel }}</span>
|
||||
</div>
|
||||
<span class="wp-ifp__spacer" />
|
||||
<Button v-if="canSendToLoop" size="sm" :disabled="pickedFrames === 0" hint="Lock the picked frames' seeds in the Context Loop and bypass the rest, so the next run repeats only your picks" data-test="image-filter-send-loop" @click="sendToLoop">Send to Loop</Button>
|
||||
<Button size="sm" variant="danger" hint="Skip everything after this node for this run" data-test="image-filter-stop" @click="stop">Stop branch</Button>
|
||||
<Button size="sm" data-test="image-filter-keep-all" @click="keepAll">Keep all {{ total }}</Button>
|
||||
<Button size="sm" variant="primary" kbd="Enter" :disabled="pickedList.length === 0" data-test="image-filter-keep-picked" @click="keepPicked">{{ pickedList.length ? `Keep ${pickedList.length} picked` : "Keep picked" }}</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* One size in every mode, so switching never moves the window under the mouse. */
|
||||
.wp-ifp { width: min(1280px, 95vw); height: 90vh; display: flex; flex-direction: column; background: var(--wp-bg2); border: 1px solid var(--wp-border); border-radius: min(6px, var(--wp-radius-cap, 999px)); overflow: hidden; color: var(--wp-text); font-size: 12px; box-shadow: var(--wp-shadow-lg); outline: none; }
|
||||
.wp-ifp__head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; flex-shrink: 0; border-bottom: 1px solid var(--wp-border); background: color-mix(in srgb, var(--wp-accent) 9%, var(--wp-bg2)); }
|
||||
.wp-ifp__head-icon { color: var(--wp-accent); width: 24px; font-size: 15px; text-align: center; }
|
||||
.wp-ifp__title-block { flex: 1; min-width: 0; }
|
||||
.wp-ifp { height: 100%; min-height: 0; display: flex; flex-direction: column; color: var(--wp-text); font-size: 12px; outline: none; }
|
||||
/* Grows past the kit header spacer so the mode switch sits at the right, by the ×. */
|
||||
.wp-ifp__title-block { flex: 999 1 auto; min-width: 0; }
|
||||
.wp-ifp__title-row { display: flex; align-items: center; gap: 8px; }
|
||||
.wp-ifp__name { font: 600 13px var(--wp-font-sans); }
|
||||
.wp-ifp__chip { font: 600 10px var(--wp-font-sans); letter-spacing: .02em; padding: 2px 6px; border-radius: min(3px, var(--wp-radius-cap, 999px)); background: color-mix(in srgb, var(--wp-accent) 22%, transparent); color: var(--wp-accent-text, var(--wp-accent)); }
|
||||
@@ -1132,8 +1142,7 @@ const subLine = computed(() => {
|
||||
.wp-ifp__full-strip .is-current { border-color: var(--wp-accent); }
|
||||
.wp-ifp__full-sep { width: 1px; height: 32px; background: var(--wp-border); }
|
||||
|
||||
.wp-ifp__foot { display: flex; align-items: center; gap: 8px; padding: 10px 14px; flex-shrink: 0; background: var(--wp-bg3); border-top: 1px solid var(--wp-border); }
|
||||
.wp-ifp__status { display: flex; gap: 12px; font: 11px var(--wp-font-sans); color: var(--wp-text-muted, var(--wp-text2)); }
|
||||
.wp-ifp__status { align-self: center; display: flex; gap: 12px; font: 11px var(--wp-font-sans); color: var(--wp-text-muted, var(--wp-text2)); }
|
||||
.wp-ifp__status strong { color: var(--wp-text); }
|
||||
.wp-ifp__edits { color: var(--wp-accent-text, var(--wp-accent)); }
|
||||
.wp-ifp__left { color: var(--wp-text-dim, var(--wp-text3)); font-family: var(--wp-font-mono, monospace); }
|
||||
|
||||
@@ -33,7 +33,7 @@ import RichTextInput from "../../manager/components/RichTextInput.vue";
|
||||
import { INLINE_PICKS_NOTE, hasInlinePicks } from "../context/editors/_shared/preview-tokens";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import ListRow from "@/kit/ListRow.vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import Popover from "@/kit/Popover.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -355,20 +355,25 @@ function onKeydown(ev: KeyboardEvent): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="true" labelledby="ibm-title" @close="onShellClose" @keydown="onKeydown">
|
||||
<div class="ibm" tabindex="-1">
|
||||
<header class="ibm__head" :style="{ '--ibm-accent': headerColor }">
|
||||
<i :class="['pi', headerIcon, 'ibm__head-icon']" aria-hidden="true" />
|
||||
<div class="ibm__title-block">
|
||||
<div class="ibm__title-row">
|
||||
<span id="ibm-title" class="ibm__name" data-test="ibm-name">{{ headerName }}</span>
|
||||
<span class="ibm__chip" data-test="ibm-chip">{{ headerType }}</span>
|
||||
</div>
|
||||
<div class="ibm__sub">Injector binding · ctx[$<wbr>{{ draftBinding || "…" }}] ← {{ templateValue ? "template" : isGeneral ? "no template yet" : "socket value" }}</div>
|
||||
<Modal
|
||||
:open="true"
|
||||
size="md"
|
||||
flush
|
||||
:accent="headerColor"
|
||||
:icon="headerIcon.replace(/^pi-/, '')"
|
||||
@update:open="(v: boolean) => { if (!v) onShellClose(); }"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<template #header="{ titleId }">
|
||||
<div class="ibm__title-block">
|
||||
<div class="ibm__title-row">
|
||||
<span :id="titleId" class="ibm__name" data-test="ibm-name">{{ headerName }}</span>
|
||||
<span class="ibm__chip" data-test="ibm-chip">{{ headerType }}</span>
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close" size="sm" kbd="Esc" data-test="ibm-close" @click="onCancel" />
|
||||
</header>
|
||||
|
||||
<div class="ibm__sub">Injector binding · ctx[$<wbr>{{ draftBinding || "…" }}] ← {{ templateValue ? "template" : isGeneral ? "no template yet" : "socket value" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ibm">
|
||||
<section class="ibm__section">
|
||||
<div class="ibm__section-head">
|
||||
<span class="ibm__section-label">Binding</span>
|
||||
@@ -564,15 +569,15 @@ function onKeydown(ev: KeyboardEvent): void {
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<footer class="ibm__foot">
|
||||
<span class="ibm__hint">
|
||||
<Kbd keys="Esc" size="sm" /> cancel · <Kbd keys="Mod+Enter" size="sm" /> save
|
||||
</span>
|
||||
<Button size="sm" data-test="ibm-cancel" @click="onCancel">Cancel</Button>
|
||||
<Button size="sm" variant="primary" :disabled="!isDirty" data-test="ibm-save" @click="onSave">Save</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<span class="ibm__hint">
|
||||
<Kbd keys="Esc" size="sm" /> cancel · <Kbd keys="Mod+Enter" size="sm" /> save
|
||||
</span>
|
||||
<Button size="sm" data-test="ibm-cancel" @click="onCancel">Cancel</Button>
|
||||
<Button size="sm" variant="primary" :disabled="!isDirty" data-test="ibm-save" @click="onSave">Save</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<!-- Shared modal-template-controls partial — imported UNSCOPED so the
|
||||
@@ -590,37 +595,12 @@ function onKeydown(ev: KeyboardEvent): void {
|
||||
|
||||
<style scoped>
|
||||
.ibm {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
background: var(--wp-bg2);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius, 6px);
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ibm__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--ibm-accent, var(--wp-accent)) 18%, var(--wp-bg2)) 0%,
|
||||
var(--wp-bg2) 100%
|
||||
);
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
}
|
||||
.ibm__head-icon {
|
||||
font-size: 18px;
|
||||
color: var(--ibm-accent, var(--wp-accent));
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.ibm__title-block { flex: 1; min-width: 0; }
|
||||
.ibm__title-block { flex: 1 1 auto; min-width: 0; }
|
||||
.ibm__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -636,8 +616,8 @@ function onKeydown(ev: KeyboardEvent): void {
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 6px;
|
||||
border-radius: min(2px, var(--wp-radius-cap, 999px));
|
||||
background: color-mix(in srgb, var(--ibm-accent, var(--wp-accent)) 22%, transparent);
|
||||
color: var(--ibm-accent, var(--wp-accent));
|
||||
background: color-mix(in srgb, var(--wp-modal-accent, var(--wp-accent)) 22%, transparent);
|
||||
color: var(--wp-modal-accent, var(--wp-accent));
|
||||
}
|
||||
.ibm__sub {
|
||||
font: 400 10px var(--wp-font-mono);
|
||||
@@ -799,15 +779,9 @@ function onKeydown(ev: KeyboardEvent): void {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ibm__foot {
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid var(--wp-border-soft, var(--wp-border));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.ibm__hint {
|
||||
margin-right: auto;
|
||||
align-self: center;
|
||||
font: 10px var(--wp-font-sans);
|
||||
color: var(--wp-text-dim, var(--wp-text3));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import WpSegmented from "@/kit/WpSegmented.vue";
|
||||
import { kindIcon } from "../shared/kind-icons";
|
||||
import { choicesFor, countLabel, type Choice, type LibraryPlan, type PlanItem } from "./note-library-plan";
|
||||
@@ -51,12 +51,14 @@ function close(result: Record<string, Choice> | null): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="visible" labelledby="wp-note-lib-title" @close="close(null)">
|
||||
<div class="wp-note-lib" data-test="note-library-dialog">
|
||||
<header class="wp-note-lib__head">
|
||||
<span id="wp-note-lib-title" class="wp-note-lib__title">Add to your library</span>
|
||||
<Modal :open="visible" size="md" flush @update:open="(v: boolean) => { if (!v) close(null); }">
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-note-lib__head">
|
||||
<span :id="titleId" class="wp-note-lib__title">Add to your library</span>
|
||||
<span class="wp-note-lib__sub">From {{ scope }}</span>
|
||||
</header>
|
||||
</div>
|
||||
</template>
|
||||
<div class="wp-note-lib" data-test="note-library-dialog">
|
||||
<div class="wp-note-lib__body">
|
||||
<section v-if="fresh.length" class="wp-note-lib__group">
|
||||
<h3 class="wp-note-lib__label">New: {{ countLabel(fresh) }}</h3>
|
||||
@@ -95,51 +97,27 @@ function close(result: Record<string, Choice> | null): void {
|
||||
</p>
|
||||
<p class="wp-note-lib__hint">One step: Undo on the message afterwards takes it all back out.</p>
|
||||
</div>
|
||||
<footer class="wp-note-lib__foot">
|
||||
<Button size="sm" data-test="note-library-cancel" @click="close(null)">Cancel</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="pi-download"
|
||||
:disabled="going.length === 0"
|
||||
data-test="note-library-add"
|
||||
@click="close(choices)"
|
||||
>{{ going.length ? `Add ${countLabel(going)}` : "Nothing to add" }}</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="sm" data-test="note-library-cancel" @click="close(null)">Cancel</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="pi-download"
|
||||
:disabled="going.length === 0"
|
||||
data-test="note-library-add"
|
||||
@click="close(choices)"
|
||||
>{{ going.length ? `Add ${countLabel(going)}` : "Nothing to add" }}</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wp-note-lib {
|
||||
width: 480px;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wp-bg);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: min(8px, var(--wp-radius-cap, 999px));
|
||||
color: var(--wp-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
.wp-note-lib__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
}
|
||||
.wp-note-lib { color: var(--wp-text); font-size: 13px; }
|
||||
.wp-note-lib__head { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }
|
||||
.wp-note-lib__title { font-size: 14px; font-weight: 600; }
|
||||
.wp-note-lib__sub { font-size: 12px; color: var(--wp-text-muted); }
|
||||
.wp-note-lib__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.wp-note-lib__body { display: flex; flex-direction: column; gap: 12px; padding: 12px 16px; }
|
||||
.wp-note-lib__group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.wp-note-lib__label {
|
||||
margin: 0;
|
||||
@@ -176,11 +154,4 @@ function close(result: Record<string, Choice> | null): void {
|
||||
}
|
||||
.wp-note-lib__note { margin: 0; font-size: 12px; color: var(--wp-text-muted); }
|
||||
.wp-note-lib__hint { margin: 0; font-size: 11px; color: var(--wp-text-dim); }
|
||||
.wp-note-lib__foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
padding: 10px 16px 12px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,8 +40,7 @@ import PlaygroundBehaviorPreview from "./PlaygroundBehaviorPreview.vue";
|
||||
import Checkbox from "@/kit/Checkbox.vue";
|
||||
import WpSelect from "@/kit/WpSelect.vue";
|
||||
import Button from "@/kit/Button.vue";
|
||||
import ModalShell from "@/kit/ModalShell.vue";
|
||||
import { useDialogFocus } from "@/kit/useDialogFocus";
|
||||
import Modal from "@/kit/Modal.vue";
|
||||
import { useHostDialogGuard } from "@/components/shared/useHostDialogGuard";
|
||||
import { vToggleRow } from "@/kit/toggle-row";
|
||||
|
||||
@@ -263,18 +262,12 @@ function onClose(): void {
|
||||
}
|
||||
|
||||
// The playground opens from ComfyUI's Settings dialog, which stays open
|
||||
// underneath. ModalShell gives the overlay, the backdrop and Escape close and
|
||||
// the key shield; useDialogFocus moves focus in, keeps Tab inside (run from
|
||||
// the shell's keydown, since the shield stops keys before window) and puts
|
||||
// focus back; the host guard, bound to the shell's overlay, stops the
|
||||
// Settings dialog from swallowing clicks, pulling focus back or closing
|
||||
// along with us.
|
||||
// underneath. Modal gives the overlay, the backdrop and Escape close, the key
|
||||
// shield and the focus trap + return; the host guard, bound to the modal's
|
||||
// overlay, stops the Settings dialog from swallowing clicks, pulling focus
|
||||
// back or closing along with us.
|
||||
const panelEl = ref<HTMLElement | null>(null);
|
||||
const overlayEl = computed(() => panelEl.value?.closest<HTMLElement>(".wp-modal-overlay") ?? null);
|
||||
const focus = useDialogFocus(panelEl, () => playgroundOpen.value, {
|
||||
onEscape: () => undefined,
|
||||
window: false,
|
||||
});
|
||||
useHostDialogGuard(overlayEl, () => playgroundOpen.value);
|
||||
|
||||
function onOpenChange(open: boolean): void {
|
||||
@@ -307,26 +300,27 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalShell :visible="playgroundOpen" labelledby="wp-pg-title" @close="onClose" @keydown="focus.onKeydown">
|
||||
<Modal
|
||||
:open="playgroundOpen"
|
||||
size="lg"
|
||||
flush
|
||||
accent="brand"
|
||||
icon="palette"
|
||||
@update:open="(v: boolean) => { if (!v) onClose(); }"
|
||||
>
|
||||
<template #header="{ titleId }">
|
||||
<div class="wp-pg__head-text">
|
||||
<h3 :id="titleId" class="wp-pg__title">Display Playground</h3>
|
||||
<p class="wp-pg__subtitle">
|
||||
Tweak Wildcard Pipeline visuals — changes apply live.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<section
|
||||
ref="panelEl"
|
||||
class="wp-pg"
|
||||
tabindex="-1"
|
||||
data-test="playground-panel"
|
||||
>
|
||||
<header class="wp-pg__head">
|
||||
<div class="wp-pg__head-inner">
|
||||
<i class="pi pi-palette wp-pg__head-icon" aria-hidden="true" />
|
||||
<div>
|
||||
<h3 id="wp-pg-title" class="wp-pg__title">Display Playground</h3>
|
||||
<p class="wp-pg__subtitle">
|
||||
Tweak Wildcard Pipeline visuals — changes apply live.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" icon="times" aria-label="Close playground" size="sm" kbd="Esc" @click="onClose" />
|
||||
</header>
|
||||
|
||||
<div class="wp-pg__body">
|
||||
<section class="wp-pg__controls" data-test="playground-controls">
|
||||
<fieldset class="wp-pg__group">
|
||||
@@ -538,12 +532,13 @@ onBeforeUnmount(() => {
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<footer class="wp-pg__foot">
|
||||
<Button size="md" @click="onResetAll">Reset all to defaults</Button>
|
||||
<Button variant="primary" size="md" @click="onClose">Done</Button>
|
||||
</footer>
|
||||
</section>
|
||||
</ModalShell>
|
||||
<template #footer>
|
||||
<Button size="md" @click="onResetAll">Reset all to defaults</Button>
|
||||
<span class="wp-pg__foot-spacer" />
|
||||
<Button variant="primary" size="md" @click="onClose">Done</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -553,52 +548,12 @@ onBeforeUnmount(() => {
|
||||
* subservient to unlayered ones in the same origin. */
|
||||
@layer wp-extension {
|
||||
.wp-pg {
|
||||
background: var(--wp-bg);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius);
|
||||
box-shadow: var(--wp-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.5));
|
||||
width: min(960px, 100%);
|
||||
max-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
color: var(--wp-text);
|
||||
outline: none;
|
||||
}
|
||||
.wp-pg__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
background: var(--wp-brand-gradient);
|
||||
position: relative;
|
||||
}
|
||||
/* Scrim that knocks the brand gradient back so the title stays readable on
|
||||
* top of it. It was hardcoded to a near-black rgba, so under the light theme
|
||||
* the banner stayed dark while `--wp-text` flipped to a DARK ink — dark text
|
||||
* on a dark band. The scrim now follows the theme: near-black under dark,
|
||||
* near-white under light, each keeping the same 0.85 knock-back. */
|
||||
.wp-pg__head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--wp-pg-head-scrim, rgba(35, 35, 35, 0.85));
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-theme-light .wp-pg__head {
|
||||
--wp-pg-head-scrim: rgba(248, 248, 250, 0.85);
|
||||
}
|
||||
.wp-theme-dark .wp-pg__head {
|
||||
--wp-pg-head-scrim: rgba(35, 35, 35, 0.85);
|
||||
}
|
||||
.wp-pg__head > * { position: relative; z-index: 1; }
|
||||
.wp-pg__head-inner { display: flex; gap: 10px; align-items: flex-start; }
|
||||
.wp-pg__head-icon {
|
||||
font-size: 14px;
|
||||
color: var(--wp-violet);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.wp-pg__head-text { flex: 1 1 auto; min-width: 0; }
|
||||
.wp-pg__title {
|
||||
margin: 0;
|
||||
font: 600 13px/1.2 var(--wp-font-sans);
|
||||
@@ -659,14 +614,7 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wp-pg__foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg2);
|
||||
}
|
||||
.wp-pg__foot-spacer { flex: 1; }
|
||||
/* Narrow viewport — stack panes */
|
||||
@media (max-width: 720px) {
|
||||
.wp-pg__body {
|
||||
|
||||
@@ -32,8 +32,8 @@ describe("DisplayPlaygroundModal", () => {
|
||||
await open();
|
||||
const dialog = document.body.querySelector('[role="dialog"]');
|
||||
expect(dialog?.getAttribute("aria-modal")).toBe("true");
|
||||
expect(dialog?.getAttribute("aria-labelledby")).toBe("wp-pg-title");
|
||||
expect(document.getElementById("wp-pg-title")?.textContent).toBe("Display Playground");
|
||||
const titleId = dialog?.getAttribute("aria-labelledby") ?? "";
|
||||
expect(document.getElementById(titleId)?.textContent).toBe("Display Playground");
|
||||
expect(document.body.querySelector('[data-test="playground-controls"]')).not.toBeNull();
|
||||
expect(document.body.querySelector('[data-test="playground-preview"]')).not.toBeNull();
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe("DisplayPlaygroundModal", () => {
|
||||
|
||||
it("Done closes it and focus goes back to what opened it", async () => {
|
||||
const trigger = await open();
|
||||
expect(document.body.querySelector(".wp-pg")?.contains(document.activeElement)).toBe(true);
|
||||
expect(document.body.querySelector('[role="dialog"]')?.contains(document.activeElement)).toBe(true);
|
||||
const done = [...document.body.querySelectorAll("button")].find((b) => b.textContent?.trim() === "Done");
|
||||
done?.click();
|
||||
await nextTick();
|
||||
|
||||
Reference in New Issue
Block a user