fix(spa): light-mode + dropdown polish for chips, pills, selects
- Add rgba() fallbacks before color-mix(in oklab) for older embedded Chromium
- Add .wp-theme-light overrides with darker text + stronger tints for chips
(accent/success/warn/danger/info), syntax pills (ref/in/dp), tag-chip
active, syntax-link missing, rich-text tokens (var/ref/quantifier),
ConstraintMatrix data-mode states, HistoryPanel active item
- Bump dynamic catChip / Chip color-mix percentages 14% to 18% bg, 35% to 42%
border (inline JS styles can't use CSS fallback pattern)
- Select.vue dropdown:
- Teleport menu to body so it escapes overflow:hidden + sticky footer
z-index; position:fixed + getBoundingClientRect placement, flip when no
space below; menu z-index 9999
- Remove min-width:0 from .wp-select__option-label so menu grows to fit
longest option (fixes 'Updated --- newest' clipping)
- Drop wp-spacer from trigger so single flex:1 element gets the space
- Update Select.test.ts to query document.body for teleported menu
- RichTextInput autocomplete:
- Remove duplicate '@' glyph (was rendering pi-at icon AND text trigger)
- Bump z-index to 9999, lighter bg, full-width head bleed
- Add color dots to category dropdowns in IdentityCard + all 6 list views
- Standardize ImportExport category chip to shared catChipStyle()
- Fix Wildcards syntax-pill--ref color: #a78bfa (kind violet) to #c026d3
(magenta, matching @ref token color)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import Select from "../../components/ui/Select.vue";
|
||||
|
||||
const opts = [
|
||||
@@ -8,33 +8,53 @@ const opts = [
|
||||
{ value: "c", label: "Gamma" },
|
||||
];
|
||||
|
||||
// The menu is teleported to <body>, so queries must use `document` rather than
|
||||
// the test-utils wrapper. `attachTo: document.body` plus `wrap.unmount()` keeps
|
||||
// teleported nodes from leaking between tests.
|
||||
|
||||
describe("Select.vue", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("opens menu on click and renders options", async () => {
|
||||
const wrap = mount(Select, { props: { modelValue: null, options: opts } });
|
||||
expect(wrap.find(".wp-select__menu").exists()).toBe(false);
|
||||
const wrap = mount(Select, {
|
||||
props: { modelValue: null, options: opts },
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(document.querySelector(".wp-select__menu")).toBeNull();
|
||||
await wrap.get("[data-test='select-trigger']").trigger("click");
|
||||
expect(wrap.find(".wp-select__menu").exists()).toBe(true);
|
||||
expect(wrap.findAll(".wp-select__option").length).toBe(3);
|
||||
expect(document.querySelector(".wp-select__menu")).not.toBeNull();
|
||||
expect(document.querySelectorAll(".wp-select__option").length).toBe(3);
|
||||
wrap.unmount();
|
||||
});
|
||||
|
||||
it("Enter on highlighted option emits update:modelValue", async () => {
|
||||
const wrap = mount(Select, { props: { modelValue: null, options: opts } });
|
||||
const wrap = mount(Select, {
|
||||
props: { modelValue: null, options: opts },
|
||||
attachTo: document.body,
|
||||
});
|
||||
const trigger = wrap.get("[data-test='select-trigger']");
|
||||
await trigger.trigger("click");
|
||||
// First option (index 0) is active by default.
|
||||
// First option (index 0) is active by default; ArrowDown moves to index 1 ("b").
|
||||
await trigger.trigger("keydown", { key: "ArrowDown" });
|
||||
await trigger.trigger("keydown", { key: "Enter" });
|
||||
const events = wrap.emitted("update:modelValue");
|
||||
expect(events).toBeTruthy();
|
||||
expect(events![0]).toEqual(["b"]);
|
||||
wrap.unmount();
|
||||
});
|
||||
|
||||
it("Esc closes the menu", async () => {
|
||||
const wrap = mount(Select, { props: { modelValue: null, options: opts } });
|
||||
const wrap = mount(Select, {
|
||||
props: { modelValue: null, options: opts },
|
||||
attachTo: document.body,
|
||||
});
|
||||
const trigger = wrap.get("[data-test='select-trigger']");
|
||||
await trigger.trigger("click");
|
||||
expect(wrap.find(".wp-select__menu").exists()).toBe(true);
|
||||
expect(document.querySelector(".wp-select__menu")).not.toBeNull();
|
||||
await trigger.trigger("keydown", { key: "Escape" });
|
||||
expect(wrap.find(".wp-select__menu").exists()).toBe(false);
|
||||
expect(document.querySelector(".wp-select__menu")).toBeNull();
|
||||
wrap.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,13 +57,15 @@ function onKey(event: KeyboardEvent) {
|
||||
<i :class="['pi', KIND_ICON[kindKey]]" aria-hidden="true" />
|
||||
{{ KIND_LABEL[kindKey] }}
|
||||
</span>
|
||||
<span v-if="atom.nsfw" class="wp-comm-card__nsfw">18+</span>
|
||||
<span
|
||||
v-if="!compatible"
|
||||
class="wp-comm-card__incompat"
|
||||
:title="`Needs engine >= ${atom.engine_min_version}`"
|
||||
>
|
||||
engine ≥ {{ atom.engine_min_version }}
|
||||
<span class="wp-comm-card__hero-right">
|
||||
<span v-if="atom.nsfw" class="wp-comm-card__nsfw">18+</span>
|
||||
<span
|
||||
v-if="!compatible"
|
||||
class="wp-comm-card__incompat"
|
||||
:title="`Needs engine >= ${atom.engine_min_version}`"
|
||||
>
|
||||
engine ≥ {{ atom.engine_min_version }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wp-comm-card__body">
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
border-radius: 999px;
|
||||
background: var(--wp-bg-3);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.wp-comm-filters__toggle--nsfw { color: var(--wp-warn); }
|
||||
|
||||
@@ -83,28 +85,27 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: var(--wp-bg-1);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius-lg);
|
||||
padding: 14px;
|
||||
}
|
||||
.wp-comm-featured__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
}
|
||||
.wp-comm-featured__label {
|
||||
display: inline-flex; gap: 8px; align-items: center;
|
||||
margin: 0; font-size: 13px; font-weight: 600;
|
||||
color: var(--wp-text);
|
||||
display: inline-flex; gap: 6px; align-items: center;
|
||||
margin: 0; font-size: 12px; font-weight: 700;
|
||||
color: var(--wp-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.wp-comm-featured__label .pi { color: var(--wp-accent-text); }
|
||||
.wp-comm-featured__hint {
|
||||
font-size: 11px;
|
||||
color: var(--wp-text-muted);
|
||||
color: var(--wp-text-dim);
|
||||
}
|
||||
.wp-comm-featured__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 1024px) { .wp-comm-featured__row { grid-template-columns: 1fr 1fr; } }
|
||||
@@ -113,7 +114,7 @@
|
||||
.wp-comm-feat {
|
||||
position: relative;
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius);
|
||||
border-radius: var(--wp-radius-lg);
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
@@ -123,17 +124,37 @@
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
padding: 0;
|
||||
transition: transform .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.wp-comm-feat:hover { transform: translateY(-1px); }
|
||||
/* Gradient overlay — rendered as pseudo so the inner flex column sits above it. */
|
||||
.wp-comm-feat::after {
|
||||
content: "";
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0) 30%, rgba(0,0,0,0.65) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.wp-comm-feat:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.35); }
|
||||
.wp-comm-feat__inner {
|
||||
position: relative; z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.45) 100%);
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.wp-comm-feat h3 { margin: 0; font-size: 16px; }
|
||||
/* Kind chip used inside featured strip — inline flex flow (NOT absolutely-positioned).
|
||||
Separate from .wp-comm-card__kind which uses position:absolute for the card grid hero. */
|
||||
.wp-comm-feat__kind {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
background: rgba(0,0,0,0.40);
|
||||
border: 1px solid rgba(255,255,255,0.18);
|
||||
color: #fff; font-size: 10px; font-weight: 600;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.wp-comm-feat h3 { margin: 0; font-size: 16px; font-weight: 700; }
|
||||
.wp-comm-feat p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
@@ -142,12 +163,12 @@
|
||||
.wp-comm-feat__stats {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.92);
|
||||
align-items: center;
|
||||
}
|
||||
.wp-comm-feat__stats i { margin-right: 4px; }
|
||||
.wp-comm-feat__stats i { margin-right: 3px; }
|
||||
|
||||
/* ---------- Card grid ---------- */
|
||||
.wp-comm-grid {
|
||||
@@ -178,36 +199,41 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.wp-comm-card__hero {
|
||||
height: 84px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
/* Kind chip in the card grid hero — part of flex flow (left side). */
|
||||
.wp-comm-card__kind {
|
||||
position: absolute; top: 8px; left: 8px;
|
||||
display: inline-flex; gap: 4px; align-items: center;
|
||||
font-size: 11px;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
background: rgba(0,0,0,0.42);
|
||||
color: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255,255,255,0.16);
|
||||
color: #fff; font-size: 10px; font-weight: 600;
|
||||
}
|
||||
/* Right-side wrapper groups nsfw + incompat badges. */
|
||||
.wp-comm-card__hero-right {
|
||||
display: flex; gap: 4px; align-items: flex-start;
|
||||
}
|
||||
.wp-comm-card__nsfw {
|
||||
position: absolute; top: 8px; right: 8px;
|
||||
display: inline-flex;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 600;
|
||||
background: var(--wp-warn);
|
||||
color: #1c1c28;
|
||||
border-radius: 999px;
|
||||
padding: 1px 6px;
|
||||
background: rgba(190, 18, 60, 0.85);
|
||||
border: 1px solid rgba(255,255,255,0.20);
|
||||
color: #fff;
|
||||
}
|
||||
.wp-comm-card__incompat {
|
||||
position: absolute; bottom: 8px; right: 8px;
|
||||
display: inline-flex;
|
||||
padding: 3px 8px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 600;
|
||||
background: rgba(239, 68, 68, 0.78);
|
||||
background: rgba(245, 158, 11, 0.85);
|
||||
border: 1px solid rgba(255,255,255,0.20);
|
||||
color: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.wp-comm-card__body {
|
||||
padding: 10px;
|
||||
padding: 10px 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -259,22 +285,26 @@
|
||||
}
|
||||
.wp-comm-card__stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--wp-text-muted);
|
||||
margin-top: 2px;
|
||||
align-items: center;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--wp-border);
|
||||
}
|
||||
.wp-comm-card__stats i { margin-right: 4px; }
|
||||
.wp-comm-card__stats span { display: inline-flex; align-items: center; gap: 3px; }
|
||||
.wp-comm-card__stats i { margin-right: 3px; }
|
||||
.wp-comm-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
.wp-comm-card__tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--wp-bg-3);
|
||||
border: 1px solid var(--wp-border);
|
||||
color: var(--wp-text-muted);
|
||||
}
|
||||
.wp-comm-card__installed {
|
||||
@@ -461,9 +491,12 @@
|
||||
.wp-comm-review__head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 12px; color: var(--wp-text-muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.wp-comm-review__head strong { color: var(--wp-text); font-size: 13px; }
|
||||
.wp-comm-review__text { margin: 6px 0 0; font-size: 13px; }
|
||||
.wp-comm-review__when { margin-left: auto; color: var(--wp-text-dim); font-size: 12px; }
|
||||
.wp-comm-review__placeholder { margin: 0; font-size: 12px; color: var(--wp-text-muted); font-style: italic; }
|
||||
.wp-comm-review__text { margin: 8px 0 0; font-size: 13px; line-height: 1.5; }
|
||||
|
||||
/* ---------- Stepper ---------- */
|
||||
.wp-comm-stepper {
|
||||
|
||||
@@ -366,20 +366,29 @@ defineExpose({ cellAt, cycleCell });
|
||||
.wp-matrix-cell[data-mode="exclude"] {
|
||||
min-width: 36px;
|
||||
padding: 0 6px;
|
||||
background: color-mix(in oklab, var(--wp-danger) 16%, transparent);
|
||||
background: rgba(239, 68, 68, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-danger) 18%, transparent);
|
||||
color: var(--wp-danger);
|
||||
border-color: color-mix(in oklab, var(--wp-danger) 35%, transparent);
|
||||
border-color: rgba(239, 68, 68, 0.40);
|
||||
border-color: color-mix(in oklab, var(--wp-danger) 40%, transparent);
|
||||
}
|
||||
.wp-matrix-cell[data-mode="boost"] {
|
||||
background: color-mix(in oklab, var(--wp-success) 16%, transparent);
|
||||
background: rgba(34, 197, 94, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-success) 18%, transparent);
|
||||
color: var(--wp-success);
|
||||
border-color: color-mix(in oklab, var(--wp-success) 35%, transparent);
|
||||
border-color: rgba(34, 197, 94, 0.40);
|
||||
border-color: color-mix(in oklab, var(--wp-success) 40%, transparent);
|
||||
}
|
||||
.wp-matrix-cell[data-mode="reduce"] {
|
||||
background: color-mix(in oklab, var(--wp-warn) 16%, transparent);
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-warn) 18%, transparent);
|
||||
color: var(--wp-warn);
|
||||
border-color: color-mix(in oklab, var(--wp-warn) 35%, transparent);
|
||||
border-color: rgba(245, 158, 11, 0.40);
|
||||
border-color: color-mix(in oklab, var(--wp-warn) 40%, transparent);
|
||||
}
|
||||
.wp-theme-light .wp-matrix-cell[data-mode="exclude"] { color: #991b1b; }
|
||||
.wp-theme-light .wp-matrix-cell[data-mode="boost"] { color: #166534; }
|
||||
.wp-theme-light .wp-matrix-cell[data-mode="reduce"] { color: #92400e; }
|
||||
.wp-matrix-cell__icon {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
/**
|
||||
* HistoryPanel
|
||||
*
|
||||
* Slide-out drawer that lists the last 3 saved snapshots of a module.
|
||||
* Each entry shows a relative timestamp, the snapshotted name, a JSON
|
||||
* preview of the snapshot's payload, and a "Restore" button.
|
||||
* Centered dialog with a master-detail two-pane layout:
|
||||
* Left (240 px) — clickable list of saved snapshots (newest first).
|
||||
* Right (1fr) — preview of the selected snapshot: date, name, raw JSON
|
||||
* and a "Restore this version" primary action.
|
||||
*
|
||||
* Click outside / Esc closes (handled by the local overlay click + window
|
||||
* keydown listener).
|
||||
*
|
||||
* The panel is layout-agnostic — it doesn't know what kind of module owns
|
||||
* the history; the parent editor wires `entries` and decides what to do
|
||||
* with the `restore` event.
|
||||
* Click outside / Esc closes. Body scroll is locked while open.
|
||||
*/
|
||||
import { computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||
import Button from "./ui/Button.vue";
|
||||
import RelativeDate from "./RelativeDate.vue";
|
||||
import type { ModuleHistoryEntry } from "../api/types";
|
||||
@@ -33,6 +29,17 @@ const ordered = computed<ModuleHistoryEntry[]>(() =>
|
||||
[...props.entries].reverse(),
|
||||
);
|
||||
|
||||
const selected = ref(0);
|
||||
|
||||
// Reset selection whenever the list changes (e.g., new save while panel is open).
|
||||
watch(ordered, () => {
|
||||
selected.value = 0;
|
||||
});
|
||||
|
||||
const currentEntry = computed(() =>
|
||||
ordered.value[selected.value] ?? null,
|
||||
);
|
||||
|
||||
function close(): void {
|
||||
emit("update:open", false);
|
||||
}
|
||||
@@ -41,6 +48,10 @@ function restore(entry: ModuleHistoryEntry): void {
|
||||
emit("restore", entry);
|
||||
}
|
||||
|
||||
function itemLabel(idx: number): string {
|
||||
return idx === 0 ? "Previous save" : `${idx + 1} saves ago`;
|
||||
}
|
||||
|
||||
function previewJson(entry: ModuleHistoryEntry): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
@@ -55,6 +66,14 @@ function previewJson(entry: ModuleHistoryEntry): string {
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotDate(entry: ModuleHistoryEntry): string {
|
||||
try {
|
||||
return new Date(entry.saved_at).toLocaleString();
|
||||
} catch {
|
||||
return entry.saved_at;
|
||||
}
|
||||
}
|
||||
|
||||
function onKey(e: KeyboardEvent): void {
|
||||
if (e.key === "Escape" && props.open) close();
|
||||
}
|
||||
@@ -66,12 +85,12 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
});
|
||||
|
||||
// Keep body from scrolling under the open panel — small UX nicety.
|
||||
watch(
|
||||
() => props.open,
|
||||
(next) => {
|
||||
if (typeof document === "undefined") return;
|
||||
document.body.style.overflow = next ? "hidden" : "";
|
||||
if (next) selected.value = 0;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@@ -84,13 +103,14 @@ watch(
|
||||
data-test="history-overlay"
|
||||
@mousedown.self="close"
|
||||
>
|
||||
<aside
|
||||
<div
|
||||
class="wp-history-panel"
|
||||
role="dialog"
|
||||
aria-label="Version history"
|
||||
data-test="history-panel"
|
||||
>
|
||||
<header class="wp-history-panel__header">
|
||||
<!-- Header -->
|
||||
<div class="wp-history-panel__header">
|
||||
<div>
|
||||
<h3 class="wp-history-panel__title">Version history</h3>
|
||||
<p class="wp-history-panel__sub">
|
||||
@@ -105,48 +125,69 @@ watch(
|
||||
data-test="history-close"
|
||||
@click="close"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div v-if="!ordered.length" class="wp-history-empty" data-test="history-empty">
|
||||
<i class="pi pi-history" />
|
||||
<p>No previous versions yet.</p>
|
||||
<p class="wp-history-empty__hint">
|
||||
Save a change to start tracking history.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div
|
||||
v-if="!ordered.length"
|
||||
class="wp-history-empty"
|
||||
data-test="history-empty"
|
||||
>
|
||||
<i class="pi pi-history" />
|
||||
<p>No previous versions yet.</p>
|
||||
<p class="wp-history-empty__hint">Save a change to start tracking history.</p>
|
||||
</div>
|
||||
|
||||
<!-- Two-pane body -->
|
||||
<div v-else class="wp-history-panel__body">
|
||||
<ol class="wp-history-list" data-test="history-list">
|
||||
<li
|
||||
<!-- Left: list of snapshots -->
|
||||
<div class="wp-history-list" data-test="history-list">
|
||||
<button
|
||||
v-for="(entry, idx) in ordered"
|
||||
:key="`${entry.saved_at}-${idx}`"
|
||||
type="button"
|
||||
class="wp-history-item"
|
||||
:data-active="idx === selected ? 'true' : 'false'"
|
||||
:data-test="`history-entry-${idx}`"
|
||||
@click="selected = idx"
|
||||
>
|
||||
<div class="wp-history-item__head">
|
||||
<div class="wp-history-item__top">
|
||||
<span class="wp-history-item__label">{{ itemLabel(idx) }}</span>
|
||||
<span class="wp-history-item__time">
|
||||
<RelativeDate :value="entry.saved_at" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="wp-history-item__name">{{ entry.name || "(untitled)" }}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right: preview pane -->
|
||||
<div class="wp-history-preview">
|
||||
<template v-if="currentEntry">
|
||||
<div class="wp-history-preview__head">
|
||||
<div>
|
||||
<div class="wp-history-item__name">
|
||||
{{ entry.name || "(untitled)" }}
|
||||
<div class="wp-history-preview__snapshot-label">
|
||||
Snapshot · {{ snapshotDate(currentEntry) }}
|
||||
</div>
|
||||
<div class="wp-history-item__meta">
|
||||
<RelativeDate :value="entry.saved_at" />
|
||||
<span class="wp-history-item__sep">·</span>
|
||||
<span :title="entry.saved_at">{{ entry.saved_at }}</span>
|
||||
<div class="wp-history-preview__name">
|
||||
{{ currentEntry.name || "Untitled" }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi-refresh"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:data-test="`history-restore-${idx}`"
|
||||
@click="restore(entry)"
|
||||
icon="pi-refresh"
|
||||
:data-test="`history-restore-${selected}`"
|
||||
@click="restore(currentEntry)"
|
||||
>Restore this version</Button>
|
||||
</div>
|
||||
<pre class="wp-history-item__json">{{ previewJson(entry) }}</pre>
|
||||
</li>
|
||||
</ol>
|
||||
<pre class="wp-history-preview__json">{{ previewJson(currentEntry) }}</pre>
|
||||
</template>
|
||||
<div v-else class="wp-dim wp-history-preview__placeholder">
|
||||
Select a snapshot to preview.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -156,27 +197,38 @@ watch(
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
background: var(--wp-overlay-bg);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
animation: wp-history-fade-in 0.15s ease-out;
|
||||
}
|
||||
@keyframes wp-history-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.wp-history-panel {
|
||||
width: 480px;
|
||||
max-width: 100vw;
|
||||
background: var(--wp-bg);
|
||||
border-left: 1px solid var(--wp-border);
|
||||
width: min(880px, 100%);
|
||||
max-height: min(640px, calc(100vh - 48px));
|
||||
background: var(--wp-bg-2);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius-lg, 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
}
|
||||
|
||||
.wp-history-panel__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
background: var(--wp-bg-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wp-history-panel__title {
|
||||
margin: 0;
|
||||
@@ -189,63 +241,115 @@ watch(
|
||||
font-size: 12px;
|
||||
color: var(--wp-text-muted);
|
||||
}
|
||||
|
||||
/* Two-pane grid body */
|
||||
.wp-history-panel__body {
|
||||
display: grid;
|
||||
grid-template-columns: 240px 1fr;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Left column — snapshot list */
|
||||
.wp-history-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-right: 1px solid var(--wp-border);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wp-history-item {
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: var(--wp-radius);
|
||||
background: var(--wp-bg-2);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: var(--wp-text);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
}
|
||||
.wp-history-item__head {
|
||||
.wp-history-item:hover {
|
||||
background: var(--wp-bg-3);
|
||||
}
|
||||
.wp-history-item[data-active="true"] {
|
||||
background: rgba(124, 58, 237, 0.20);
|
||||
background: color-mix(in oklab, var(--wp-accent-600) 20%, transparent);
|
||||
border-color: rgba(139, 92, 246, 0.50);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 50%, transparent);
|
||||
}
|
||||
.wp-history-item__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: baseline;
|
||||
font-size: 11.5px;
|
||||
color: var(--wp-text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.wp-history-item__label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wp-history-item__time {
|
||||
font-family: var(--wp-font-mono, ui-monospace, monospace);
|
||||
}
|
||||
.wp-history-item__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--wp-text);
|
||||
word-break: break-word;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wp-history-item__meta {
|
||||
|
||||
/* Right column — preview pane */
|
||||
.wp-history-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wp-history-preview__head {
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--wp-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wp-history-preview__snapshot-label {
|
||||
font-size: 11px;
|
||||
color: var(--wp-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.wp-history-preview__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-top: 2px;
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-history-item__sep {
|
||||
color: var(--wp-text-dim, #6e6e7c);
|
||||
}
|
||||
.wp-history-item__json {
|
||||
.wp-history-preview__json {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
max-height: 240px;
|
||||
padding: 14px 16px;
|
||||
overflow: auto;
|
||||
font-family: var(--wp-font-mono, ui-monospace, monospace);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.4;
|
||||
color: var(--wp-text-muted);
|
||||
background: var(--wp-bg);
|
||||
border: 1px solid var(--wp-border);
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
color: var(--wp-text);
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.wp-history-preview__placeholder {
|
||||
padding: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.wp-history-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -53,7 +53,7 @@ const categoryStore = useCategoryStore();
|
||||
|
||||
const categoryOptions = computed<SelectOption[]>(() => [
|
||||
{ value: null, label: "None" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
// `varBinding` auto-tracks slugified name until the user touches it.
|
||||
|
||||
@@ -364,8 +364,9 @@ onBeforeUnmount(() => {
|
||||
@mousedown="(e) => onSuggestionMouseDown(e, label)"
|
||||
@mouseenter="acActive = i"
|
||||
>
|
||||
<i :class="acTrigger === '$' ? 'pi pi-dollar' : 'pi pi-at'" class="wp-rt-suggestions__icon" />
|
||||
<span class="wp-rt-suggestions__trigger">{{ acTrigger }}</span>{{ label }}
|
||||
<span class="wp-rt-suggestions__label">
|
||||
<span class="wp-rt-suggestions__trigger">{{ acTrigger }}</span>{{ label }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
@@ -476,15 +477,15 @@ onBeforeUnmount(() => {
|
||||
* ------------------------------------------------------------------------ */
|
||||
.wp-rt-suggestions {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
z-index: 9999;
|
||||
min-width: 200px;
|
||||
max-width: 360px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
background: var(--wp-bg-1, #11111b);
|
||||
background: var(--wp-bg-2, #15151f);
|
||||
border: 1px solid var(--wp-border-strong, rgba(255, 255, 255, 0.14));
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
padding: 0 4px 4px;
|
||||
box-shadow: var(--wp-shadow-lg, var(--wp-shadow, 0 10px 30px rgba(0, 0, 0, 0.45)));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -494,7 +495,6 @@ onBeforeUnmount(() => {
|
||||
animation: wp-rt-suggestions-in 0.12s ease-out;
|
||||
}
|
||||
.wp-rt-suggestions--up {
|
||||
/* `top` is set inline as the rect.top of the input; shift fully above. */
|
||||
transform: translateY(-100%);
|
||||
animation-name: wp-rt-suggestions-in-up;
|
||||
}
|
||||
@@ -508,12 +508,15 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.wp-rt-suggestions__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--wp-text-muted, #a1a1ad);
|
||||
color: var(--wp-text-dim, #6e6e7c);
|
||||
background: var(--wp-bg-3, #1e1e2a);
|
||||
border-bottom: 1px solid var(--wp-border, rgba(255, 255, 255, 0.08));
|
||||
margin-bottom: 2px;
|
||||
margin: 0 -4px 4px;
|
||||
border-radius: 7px 7px 0 0;
|
||||
}
|
||||
.wp-rt-suggestions__query {
|
||||
font-family: var(--wp-font-mono, ui-monospace, monospace);
|
||||
@@ -521,30 +524,32 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.wp-rt-suggestions__hint {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
opacity: 0.6;
|
||||
font-family: var(--wp-font, system-ui, sans-serif);
|
||||
}
|
||||
.wp-rt-suggestions__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
padding: 7px 10px;
|
||||
font-family: var(--wp-font-mono, ui-monospace, monospace);
|
||||
font-size: 12px;
|
||||
font-size: 12.5px;
|
||||
color: var(--wp-text, #e7e7ee);
|
||||
cursor: pointer;
|
||||
}
|
||||
.wp-rt-suggestions__item[data-active] {
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 22%, transparent);
|
||||
}
|
||||
.wp-rt-suggestions__icon {
|
||||
font-size: 10px;
|
||||
color: var(--wp-accent-text, #c4b5fd);
|
||||
opacity: 0.85;
|
||||
.wp-rt-suggestions__label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wp-rt-suggestions__trigger {
|
||||
color: var(--wp-accent-text, #c4b5fd);
|
||||
|
||||
@@ -32,8 +32,8 @@ const colorStyle = computed(() => {
|
||||
const c = props.color;
|
||||
return {
|
||||
color: c,
|
||||
background: `color-mix(in oklab, ${c} 14%, transparent)`,
|
||||
borderColor: `color-mix(in oklab, ${c} 35%, transparent)`,
|
||||
background: `color-mix(in oklab, ${c} 18%, transparent)`,
|
||||
borderColor: `color-mix(in oklab, ${c} 42%, transparent)`,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import Icon from "./Icon.vue";
|
||||
export interface SelectOption {
|
||||
value: string | number | null;
|
||||
label: string;
|
||||
/** Optional color dot shown before the label (e.g. category color). */
|
||||
dot?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -29,6 +31,9 @@ const open = ref(false);
|
||||
const flip = ref(false);
|
||||
const active = ref(0);
|
||||
|
||||
/** Fixed-position coordinates for the teleported menu. */
|
||||
const menuStyle = ref<Record<string, string>>({});
|
||||
|
||||
const selected = computed(() => props.options.find((o) => o.value === props.modelValue) ?? null);
|
||||
|
||||
const btnClasses = computed(() => [
|
||||
@@ -37,28 +42,66 @@ const btnClasses = computed(() => [
|
||||
]);
|
||||
|
||||
function onDocClick(e: MouseEvent) {
|
||||
if (!wrapRef.value) return;
|
||||
if (!wrapRef.value.contains(e.target as Node)) open.value = false;
|
||||
const t = e.target as Node | null;
|
||||
if (!t) return;
|
||||
// Close when click is outside the trigger button AND outside the menu.
|
||||
if (wrapRef.value?.contains(t)) return;
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("mousedown", onDocClick);
|
||||
document.addEventListener("mousedown", onDocClick, true);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("mousedown", onDocClick);
|
||||
document.removeEventListener("mousedown", onDocClick, true);
|
||||
window.removeEventListener("scroll", closeOnScroll, true);
|
||||
window.removeEventListener("resize", reposition);
|
||||
});
|
||||
|
||||
watch(open, (v) => {
|
||||
if (!v || !btnRef.value) return;
|
||||
// Decide if the menu should flip above the button.
|
||||
function closeOnScroll() { open.value = false; }
|
||||
function reposition() {
|
||||
if (!open.value || !btnRef.value) return;
|
||||
computeMenuStyle();
|
||||
}
|
||||
|
||||
function computeMenuStyle() {
|
||||
if (!btnRef.value) return;
|
||||
const rect = btnRef.value.getBoundingClientRect();
|
||||
const need = Math.min(244, props.options.length * 36 + 12);
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const need = Math.min(240, props.options.length * 30 + 12);
|
||||
flip.value = spaceBelow < need + 16 && spaceAbove > spaceBelow;
|
||||
|
||||
menuStyle.value = flip.value
|
||||
? {
|
||||
position: "fixed",
|
||||
bottom: (window.innerHeight - rect.top + 4) + "px",
|
||||
top: "auto",
|
||||
left: rect.left + "px",
|
||||
minWidth: rect.width + "px",
|
||||
}
|
||||
: {
|
||||
position: "fixed",
|
||||
top: (rect.bottom + 4) + "px",
|
||||
bottom: "auto",
|
||||
left: rect.left + "px",
|
||||
minWidth: rect.width + "px",
|
||||
};
|
||||
}
|
||||
|
||||
watch(open, (v) => {
|
||||
if (!v) {
|
||||
window.removeEventListener("scroll", closeOnScroll, true);
|
||||
window.removeEventListener("resize", reposition);
|
||||
return;
|
||||
}
|
||||
if (!btnRef.value) return;
|
||||
computeMenuStyle();
|
||||
// Highlight currently-selected option (or first).
|
||||
const idx = props.options.findIndex((o) => o.value === props.modelValue);
|
||||
active.value = idx >= 0 ? idx : 0;
|
||||
window.addEventListener("scroll", closeOnScroll, true);
|
||||
window.addEventListener("resize", reposition);
|
||||
});
|
||||
|
||||
function toggle() {
|
||||
@@ -119,9 +162,12 @@ function onKeydown(e: KeyboardEvent) {
|
||||
@click="toggle"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<span v-if="selected">{{ selected.label }}</span>
|
||||
<span v-else class="wp-select__placeholder">{{ placeholder }}</span>
|
||||
<span class="wp-spacer" />
|
||||
<!-- Selected label — optional color dot prefix. -->
|
||||
<span class="wp-select__label-wrap">
|
||||
<span v-if="selected?.dot" class="wp-select__dot" :style="{ background: selected.dot }" />
|
||||
<span v-if="selected" class="wp-select__label-text">{{ selected.label }}</span>
|
||||
<span v-else class="wp-select__placeholder">{{ placeholder }}</span>
|
||||
</span>
|
||||
<button
|
||||
v-if="clearable && selected"
|
||||
type="button"
|
||||
@@ -133,29 +179,36 @@ function onKeydown(e: KeyboardEvent) {
|
||||
</button>
|
||||
<Icon class="wp-select__chevron" name="chevron-down" />
|
||||
</button>
|
||||
<ul
|
||||
v-if="open"
|
||||
class="wp-select__menu"
|
||||
role="listbox"
|
||||
:data-flip="flip ? 'true' : 'false'"
|
||||
>
|
||||
<li
|
||||
v-for="(opt, i) in options"
|
||||
:key="String(opt.value)"
|
||||
class="wp-select__option"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
:aria-selected="opt.value === modelValue"
|
||||
:data-active="i === active ? 'true' : 'false'"
|
||||
:data-selected="opt.value === modelValue ? 'true' : 'false'"
|
||||
@mousedown.prevent="pick(opt)"
|
||||
@mouseenter="active = i"
|
||||
@focusin="active = i"
|
||||
|
||||
<!-- Menu teleported to <body> so it escapes any overflow:hidden ancestor
|
||||
and renders above the sticky footer regardless of z-index stacking contexts. -->
|
||||
<Teleport to="body">
|
||||
<ul
|
||||
v-if="open"
|
||||
class="wp-select__menu"
|
||||
role="listbox"
|
||||
:style="menuStyle"
|
||||
data-test="select-menu"
|
||||
>
|
||||
{{ opt.label }}
|
||||
<span class="wp-spacer" />
|
||||
<Icon v-if="opt.value === modelValue" name="check" :size="11" />
|
||||
</li>
|
||||
</ul>
|
||||
<li
|
||||
v-for="(opt, i) in options"
|
||||
:key="String(opt.value)"
|
||||
class="wp-select__option"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
:aria-selected="opt.value === modelValue"
|
||||
:data-active="i === active ? 'true' : 'false'"
|
||||
:data-selected="opt.value === modelValue ? 'true' : 'false'"
|
||||
@mousedown.prevent="pick(opt)"
|
||||
@mouseenter="active = i"
|
||||
@focusin="active = i"
|
||||
>
|
||||
<span v-if="opt.dot" class="wp-select__dot" :style="{ background: opt.dot }" />
|
||||
<span class="wp-select__option-label">{{ opt.label }}</span>
|
||||
<span class="wp-spacer" />
|
||||
<Icon v-if="opt.value === modelValue" name="check" :size="11" />
|
||||
</li>
|
||||
</ul>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
.wp-rt .wp-rt-var,
|
||||
.wp-rt-preview .wp-rt-var {
|
||||
color: var(--wp-accent-text-strong, var(--wp-accent-text, #c4b5fd));
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 15%, transparent);
|
||||
background: rgba(139, 92, 246, 0.16);
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 16%, transparent);
|
||||
box-shadow: inset 0 0 0 1px rgba(139, 92, 246, 0.32);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 32%, transparent);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -43,7 +46,10 @@
|
||||
.wp-rt .wp-rt-ref,
|
||||
.wp-rt-preview .wp-rt-ref {
|
||||
color: var(--wp-kind-wildcard, #f0abfc);
|
||||
background: color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 12%, transparent);
|
||||
background: rgba(192, 38, 211, 0.15);
|
||||
background: color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 15%, transparent);
|
||||
box-shadow: inset 0 0 0 1px rgba(192, 38, 211, 0.30);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 30%, transparent);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -73,7 +79,8 @@
|
||||
.wp-rt .wp-rt-quantifier,
|
||||
.wp-rt-preview .wp-rt-quantifier {
|
||||
color: var(--wp-info, #60a5fa);
|
||||
background: color-mix(in oklab, var(--wp-info, #3b82f6) 14%, transparent);
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
background: color-mix(in oklab, var(--wp-info, #3b82f6) 16%, transparent);
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -86,17 +93,18 @@
|
||||
.wp-rt--focused .wp-rt-var,
|
||||
.wp-rt--focused .wp-rt-ref {
|
||||
padding: 0;
|
||||
box-shadow: inset 0 0 0 1px var(--wp-accent-500, #8b5cf6);
|
||||
}
|
||||
.wp-rt--rest .wp-rt-var,
|
||||
.wp-rt--rest .wp-rt-ref {
|
||||
padding: 0 4px;
|
||||
margin: 0 0.5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.wp-rt--rest .wp-rt-dp-multi,
|
||||
.wp-rt--rest .wp-rt-dp-weight,
|
||||
.wp-rt--rest .wp-rt-quantifier {
|
||||
padding: 0 3px;
|
||||
margin: 0 0.5px;
|
||||
}
|
||||
|
||||
/* Read-only preview always renders chips at rest. Match the
|
||||
@@ -111,3 +119,22 @@
|
||||
.wp-rt-preview .wp-rt-quantifier {
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
/* ── Light-mode overrides: rich-text token chrome ── */
|
||||
.wp-theme-light .wp-rt .wp-rt-var,
|
||||
.wp-theme-light .wp-rt-preview .wp-rt-var {
|
||||
/* color auto-adapts (--wp-accent-text-strong → accent-800 in light mode) */
|
||||
background: rgba(139, 92, 246, 0.13);
|
||||
box-shadow: inset 0 0 0 1px rgba(139, 92, 246, 0.36);
|
||||
}
|
||||
.wp-theme-light .wp-rt .wp-rt-ref,
|
||||
.wp-theme-light .wp-rt-preview .wp-rt-ref {
|
||||
color: #86198f;
|
||||
background: rgba(192, 38, 211, 0.13);
|
||||
box-shadow: inset 0 0 0 1px rgba(192, 38, 211, 0.36);
|
||||
}
|
||||
.wp-theme-light .wp-rt .wp-rt-quantifier,
|
||||
.wp-theme-light .wp-rt-preview .wp-rt-quantifier {
|
||||
color: #1d4ed8;
|
||||
background: rgba(59, 130, 246, 0.13);
|
||||
}
|
||||
|
||||
+123
-27
@@ -452,6 +452,17 @@ input, textarea, select { font: inherit; }
|
||||
}
|
||||
.wp-input::placeholder, .wp-textarea::placeholder { color: var(--wp-text-dim); }
|
||||
.wp-input--sm, .wp-select--sm { height: var(--wp-input-h-sm); font-size: 12px; padding: 0 8px; }
|
||||
/* Custom select trigger button — flex layout so label + spacer + chevron align. */
|
||||
.wp-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wp-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
||||
.wp-field__label {
|
||||
@@ -520,13 +531,16 @@ input, textarea, select { font: inherit; }
|
||||
}
|
||||
.wp-chip--accent {
|
||||
color: var(--wp-accent-text-strong);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 35%, transparent);
|
||||
background: rgba(139, 92, 246, 0.16);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 16%, transparent);
|
||||
border-color: rgba(139, 92, 246, 0.38);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 38%, transparent);
|
||||
}
|
||||
.wp-chip--success { color: #86efac; background: rgba(34,197,94,0.12); border-color: rgba(34,197,94,0.32); }
|
||||
.wp-chip--warn { color: #fcd34d; background: rgba(245,158,11,0.12); border-color: rgba(245,158,11,0.32); }
|
||||
.wp-chip--danger { color: #fca5a5; background: rgba(239,68,68,0.12); border-color: rgba(239,68,68,0.32); }
|
||||
.wp-chip--info { color: #93c5fd; background: rgba(59,130,246,0.12); border-color: rgba(59,130,246,0.32); }
|
||||
/* dark-mode text is intentionally light; light-mode overrides below */
|
||||
.wp-chip--success { color: #86efac; background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.36); }
|
||||
.wp-chip--warn { color: #fcd34d; background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.36); }
|
||||
.wp-chip--danger { color: #fca5a5; background: rgba(239,68,68,0.14); border-color: rgba(239,68,68,0.36); }
|
||||
.wp-chip--info { color: #93c5fd; background: rgba(59,130,246,0.14); border-color: rgba(59,130,246,0.36); }
|
||||
.wp-chip__dot { width: 7px; height: 7px; border-radius: 999px; background: currentColor; }
|
||||
.wp-chip__close {
|
||||
background: none; border: none; padding: 0; margin-left: 2px;
|
||||
@@ -602,6 +616,15 @@ input, textarea, select { font: inherit; }
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
.wp-tag-chip:hover { background: var(--wp-bg-4); color: var(--wp-text); }
|
||||
/* Active state defined globally so scoped view files can use color-mix and this
|
||||
serves as the rgba fallback when color-mix is unsupported (old Chromium). */
|
||||
.wp-tag-chip[data-active="true"] {
|
||||
background: rgba(139, 92, 246, 0.22);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 22%, transparent);
|
||||
border-color: rgba(139, 92, 246, 0.45);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 45%, transparent);
|
||||
color: var(--wp-accent-text);
|
||||
}
|
||||
|
||||
.wp-id {
|
||||
font-family: var(--wp-font-mono);
|
||||
@@ -690,20 +713,67 @@ input, textarea, select { font: inherit; }
|
||||
.wp-syntax-pill--ref {
|
||||
/* outgoing nested @refs — pink/magenta to match @ref token color */
|
||||
color: #f0abfc;
|
||||
background: color-mix(in oklab, #c026d3 14%, transparent);
|
||||
border-color: color-mix(in oklab, #c026d3 28%, transparent);
|
||||
background: rgba(192, 38, 211, 0.18);
|
||||
background: color-mix(in oklab, #c026d3 18%, transparent);
|
||||
border-color: rgba(192, 38, 211, 0.32);
|
||||
border-color: color-mix(in oklab, #c026d3 32%, transparent);
|
||||
}
|
||||
.wp-syntax-pill--in {
|
||||
/* incoming references — accent (purple) */
|
||||
color: var(--wp-accent-text);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 32%, transparent);
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 18%, transparent);
|
||||
border-color: rgba(139, 92, 246, 0.35);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 35%, transparent);
|
||||
}
|
||||
.wp-syntax-pill--dp {
|
||||
/* inline {a|b|c} — yellow to match dp-brace token color */
|
||||
color: #fcd34d;
|
||||
background: color-mix(in oklab, #facc15 14%, transparent);
|
||||
border-color: color-mix(in oklab, #facc15 30%, transparent);
|
||||
background: rgba(250, 204, 21, 0.18);
|
||||
background: color-mix(in oklab, #facc15 18%, transparent);
|
||||
border-color: rgba(250, 204, 21, 0.32);
|
||||
border-color: color-mix(in oklab, #facc15 32%, transparent);
|
||||
}
|
||||
|
||||
/* Light-mode pill overrides — dark text + stronger tint for contrast on near-white surface */
|
||||
.wp-theme-light .wp-syntax-pill--ref {
|
||||
color: #86198f;
|
||||
background: rgba(192, 38, 211, 0.13);
|
||||
border-color: rgba(192, 38, 211, 0.38);
|
||||
}
|
||||
.wp-theme-light .wp-syntax-pill--in {
|
||||
/* color inherits --wp-accent-text which light mode sets to accent-700 (#6d28d9) */
|
||||
background: rgba(139, 92, 246, 0.13);
|
||||
border-color: rgba(139, 92, 246, 0.40);
|
||||
}
|
||||
.wp-theme-light .wp-syntax-pill--dp {
|
||||
color: #92400e;
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
border-color: rgba(234, 179, 8, 0.45);
|
||||
}
|
||||
|
||||
/* ── Light-mode overrides: chips, tags, links ── */
|
||||
/* Tone chips: swap light text for dark text, slightly stronger tint */
|
||||
.wp-theme-light .wp-chip--accent {
|
||||
/* color inherits --wp-accent-text-strong → accent-800 in light mode */
|
||||
background: rgba(139, 92, 246, 0.13);
|
||||
border-color: rgba(139, 92, 246, 0.40);
|
||||
}
|
||||
.wp-theme-light .wp-chip--success { color: #166534; background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.42); }
|
||||
.wp-theme-light .wp-chip--warn { color: #92400e; background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.42); }
|
||||
.wp-theme-light .wp-chip--danger { color: #991b1b; background: rgba(239,68,68,0.14); border-color: rgba(239,68,68,0.42); }
|
||||
.wp-theme-light .wp-chip--info { color: #1e40af; background: rgba(59,130,246,0.14); border-color: rgba(59,130,246,0.42); }
|
||||
/* Tag chip active: color auto-adapts (--wp-accent-text → dark purple in light mode) */
|
||||
.wp-theme-light .wp-tag-chip[data-active="true"] {
|
||||
background: rgba(139, 92, 246, 0.16);
|
||||
border-color: rgba(139, 92, 246, 0.42);
|
||||
}
|
||||
/* Missing wildcard ref link */
|
||||
.wp-theme-light .wp-syntax-link--missing,
|
||||
.wp-theme-light .wp-syntax-link--missing:hover {
|
||||
color: #b91c1c;
|
||||
border-color: rgba(239, 68, 68, 0.42);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
/* Reference graph block inside row-expand */
|
||||
@@ -736,20 +806,26 @@ input, textarea, select { font: inherit; }
|
||||
}
|
||||
.wp-syntax-link:hover {
|
||||
color: var(--wp-accent-text);
|
||||
border-color: rgba(139, 92, 246, 0.45);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 45%, transparent);
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 12%, var(--wp-bg-3));
|
||||
}
|
||||
.wp-syntax-link--missing {
|
||||
color: #f87171;
|
||||
border-color: color-mix(in oklab, #f87171 35%, transparent);
|
||||
background: color-mix(in oklab, #f87171 12%, transparent);
|
||||
border-color: rgba(248, 113, 113, 0.38);
|
||||
border-color: color-mix(in oklab, #f87171 38%, transparent);
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
background: color-mix(in oklab, #f87171 14%, transparent);
|
||||
font-family: var(--wp-font-mono);
|
||||
cursor: default;
|
||||
}
|
||||
.wp-syntax-link--missing:hover {
|
||||
color: #f87171;
|
||||
border-color: color-mix(in oklab, #f87171 35%, transparent);
|
||||
background: color-mix(in oklab, #f87171 12%, transparent);
|
||||
border-color: rgba(248, 113, 113, 0.38);
|
||||
border-color: color-mix(in oklab, #f87171 38%, transparent);
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
background: color-mix(in oklab, #f87171 14%, transparent);
|
||||
}
|
||||
|
||||
.wp-id {
|
||||
@@ -1353,25 +1429,20 @@ input, textarea, select { font: inherit; }
|
||||
|
||||
/* ---------- Custom select popup ---------- */
|
||||
.wp-select-wrap { position: relative; display: inline-block; width: 100%; }
|
||||
|
||||
/* Menu teleported to <body>; position/top/left provided via inline style from JS. */
|
||||
.wp-select__menu {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
top: calc(100% + 4px);
|
||||
background: var(--wp-bg-3);
|
||||
border: 1px solid var(--wp-border-strong);
|
||||
border-radius: 7px;
|
||||
box-shadow: var(--wp-shadow-lg);
|
||||
z-index: 50;
|
||||
z-index: 9999;
|
||||
padding: 4px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
.wp-select__menu[data-flip="true"] {
|
||||
top: auto;
|
||||
bottom: calc(100% + 4px);
|
||||
}
|
||||
.wp-select__option {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 6px 8px;
|
||||
@@ -1386,15 +1457,40 @@ input, textarea, select { font: inherit; }
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-select__option[data-active="true"] {
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 18%, transparent);
|
||||
background: rgba(139, 92, 246, 0.20);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 20%, transparent);
|
||||
color: var(--wp-text);
|
||||
}
|
||||
.wp-select__option[data-selected="true"] {
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 16%, transparent);
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 18%, transparent);
|
||||
color: var(--wp-text);
|
||||
}
|
||||
/* Dot + label wrapper inside trigger button */
|
||||
.wp-select__label-wrap {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
flex: 1; min-width: 0; overflow: hidden;
|
||||
}
|
||||
/* Color dot — rendered in both trigger and option list */
|
||||
.wp-select__dot {
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
/* Label text inside trigger — must be a flex item (not inline) for ellipsis to work */
|
||||
.wp-select__label-text,
|
||||
.wp-select__placeholder {
|
||||
flex: 1; min-width: 0;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
/* Option label text — no truncation; menu grows to fit widest option */
|
||||
.wp-select__option-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wp-select__placeholder { color: var(--wp-text-dim); }
|
||||
.wp-select__chevron { color: var(--wp-text-dim); font-size: 11px; margin-left: 6px; }
|
||||
.wp-select__chevron { color: var(--wp-text-dim); font-size: 11px; margin-left: 6px; flex-shrink: 0; }
|
||||
|
||||
/* ---------- Color picker (categories) ---------- */
|
||||
.wp-color-picker {
|
||||
|
||||
@@ -17,7 +17,7 @@ export function catChipStyle(color?: string | null): Record<string, string> {
|
||||
}
|
||||
return {
|
||||
color,
|
||||
background: `color-mix(in oklab, ${color} 14%, transparent)`,
|
||||
borderColor: `color-mix(in oklab, ${color} 35%, transparent)`,
|
||||
background: `color-mix(in oklab, ${color} 18%, transparent)`,
|
||||
borderColor: `color-mix(in oklab, ${color} 42%, transparent)`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -36,7 +36,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -280,9 +280,9 @@ function actView(a: DerivationAction | undefined): { verb: string; target: strin
|
||||
|
||||
.wp-rules-snippet {
|
||||
max-width: 720px;
|
||||
line-height: 1.7;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.wp-rule-block { margin-bottom: 8px; }
|
||||
.wp-rule-block { margin-bottom: 6px; }
|
||||
.wp-rule-block:last-child { margin-bottom: 0; }
|
||||
.wp-rule-block__head {
|
||||
font-size: 10.5px;
|
||||
|
||||
@@ -29,7 +29,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import Icon from "../components/ui/Icon.vue";
|
||||
import Input from "../components/ui/Input.vue";
|
||||
import Select from "../components/ui/Select.vue";
|
||||
import { useToast } from "../composables/useToast";
|
||||
import { catChipStyle } from "../utils/catChip";
|
||||
import { api } from "../api/client";
|
||||
import type { CategoryRow, ImportBundle, ModuleRow } from "../api/types";
|
||||
import {
|
||||
@@ -609,8 +610,8 @@ watch(
|
||||
<span class="wp-io-row__name">{{ row.name }}</span>
|
||||
<span
|
||||
v-if="row.category_id && categoryById.get(row.category_id)"
|
||||
class="wp-io-row__catchip"
|
||||
:style="{ background: categoryById.get(row.category_id)?.color || 'var(--wp-bg-3)' }"
|
||||
class="wp-cat-chip"
|
||||
:style="catChipStyle(categoryById.get(row.category_id)!.color)"
|
||||
>{{ categoryById.get(row.category_id)?.name }}</span>
|
||||
<span class="wp-id">{{ row.id }}</span>
|
||||
</div>
|
||||
@@ -890,16 +891,6 @@ watch(
|
||||
}
|
||||
.wp-io-row:hover { background: var(--wp-bg-2); }
|
||||
.wp-io-row__name { flex: 1; font-weight: 500; }
|
||||
.wp-io-row__catchip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 0 rgba(0,0,0,.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wp-io-row__empty {
|
||||
padding: 10px 12px 10px 36px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -41,7 +41,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ const allTags = computed(() => {
|
||||
|
||||
const categoryOptions = computed(() => [
|
||||
{ value: null, label: "All categories" },
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name })),
|
||||
...categoryStore.items.map((c) => ({ value: c.id, label: c.name, dot: c.color || undefined })),
|
||||
]);
|
||||
|
||||
// Reactive ref-graph computed off the store. Rebuilds on every change since
|
||||
@@ -376,19 +376,42 @@ function isValid(row: ModuleRow): boolean {
|
||||
}
|
||||
.wp-syntax-pill .pi { font-size: 9.5px; }
|
||||
.wp-syntax-pill--ref {
|
||||
color: var(--wp-kind-wildcard, #f0abfc);
|
||||
background: color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 28%, transparent);
|
||||
/* @ref token color — pink/magenta, intentionally distinct from wildcard KIND violet */
|
||||
color: #f0abfc;
|
||||
background: rgba(192, 38, 211, 0.18);
|
||||
background: color-mix(in oklab, #c026d3 18%, transparent);
|
||||
border-color: rgba(192, 38, 211, 0.32);
|
||||
border-color: color-mix(in oklab, #c026d3 32%, transparent);
|
||||
}
|
||||
.wp-syntax-pill--in {
|
||||
color: var(--wp-accent-text, #c4b5fd);
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 32%, transparent);
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 18%, transparent);
|
||||
border-color: rgba(139, 92, 246, 0.35);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 35%, transparent);
|
||||
}
|
||||
.wp-syntax-pill--dp {
|
||||
color: var(--wp-warn, #fcd34d);
|
||||
background: color-mix(in oklab, var(--wp-warn, #facc15) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-warn, #facc15) 30%, transparent);
|
||||
background: rgba(250, 204, 21, 0.18);
|
||||
background: color-mix(in oklab, var(--wp-warn, #facc15) 18%, transparent);
|
||||
border-color: rgba(250, 204, 21, 0.32);
|
||||
border-color: color-mix(in oklab, var(--wp-warn, #facc15) 32%, transparent);
|
||||
}
|
||||
|
||||
/* Light-mode overrides — must repeat here because scoped [data-v-xxx] wins specificity */
|
||||
.wp-theme-light .wp-syntax-pill--ref {
|
||||
color: #86198f;
|
||||
background: rgba(192, 38, 211, 0.13);
|
||||
border-color: rgba(192, 38, 211, 0.38);
|
||||
}
|
||||
.wp-theme-light .wp-syntax-pill--in {
|
||||
background: rgba(139, 92, 246, 0.13);
|
||||
border-color: rgba(139, 92, 246, 0.40);
|
||||
}
|
||||
.wp-theme-light .wp-syntax-pill--dp {
|
||||
color: #92400e;
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
border-color: rgba(234, 179, 8, 0.45);
|
||||
}
|
||||
|
||||
.wp-opts-grid {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useRouter } from "vue-router";
|
||||
import Button from "../../components/ui/Button.vue";
|
||||
import Input from "../../components/ui/Input.vue";
|
||||
import Select from "../../components/ui/Select.vue";
|
||||
import Checkbox from "../../components/ui/Checkbox.vue";
|
||||
import { useCommunityStore } from "../../stores/communityStore";
|
||||
import CommunityCard from "../../community/CommunityCard.vue";
|
||||
import { KIND_ICON, KIND_LABEL, fmtNumber } from "../../community/format";
|
||||
@@ -132,7 +131,7 @@ onMounted(async () => {
|
||||
@click="openDetail(atom.id)"
|
||||
>
|
||||
<div class="wp-comm-feat__inner">
|
||||
<span class="wp-comm-card__kind">
|
||||
<span class="wp-comm-feat__kind">
|
||||
<i :class="['pi', KIND_ICON[atom.type === 'bundle' ? 'bundle' : atom.type]]" aria-hidden="true" />
|
||||
{{ KIND_LABEL[atom.type === "bundle" ? "bundle" : atom.type] }}
|
||||
</span>
|
||||
@@ -174,15 +173,45 @@ onMounted(async () => {
|
||||
aria-label="Sort modules"
|
||||
/>
|
||||
</div>
|
||||
<label class="wp-comm-filters__toggle">
|
||||
<Checkbox v-model="verifiedOnly" /> Verified only
|
||||
</label>
|
||||
<label class="wp-comm-filters__toggle">
|
||||
<Checkbox v-model="compatibleOnly" /> Compatible (engine {{ store.engineVersion }})
|
||||
</label>
|
||||
<label class="wp-comm-filters__toggle wp-comm-filters__toggle--nsfw">
|
||||
<Checkbox v-model="includeNsfw" /> Show 18+
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="wp-comm-filters__toggle"
|
||||
:data-active="verifiedOnly ? 'true' : 'false'"
|
||||
@click="verifiedOnly = !verifiedOnly"
|
||||
>
|
||||
<span class="wp-check" :data-checked="verifiedOnly ? 'true' : 'false'" aria-hidden="true">
|
||||
<svg v-if="verifiedOnly" viewBox="0 0 12 12" fill="none" style="display:block">
|
||||
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
Verified only
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="wp-comm-filters__toggle"
|
||||
:data-active="compatibleOnly ? 'true' : 'false'"
|
||||
@click="compatibleOnly = !compatibleOnly"
|
||||
>
|
||||
<span class="wp-check" :data-checked="compatibleOnly ? 'true' : 'false'" aria-hidden="true">
|
||||
<svg v-if="compatibleOnly" viewBox="0 0 12 12" fill="none" style="display:block">
|
||||
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
Compatible (engine {{ store.engineVersion }})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="wp-comm-filters__toggle wp-comm-filters__toggle--nsfw"
|
||||
:data-active="includeNsfw ? 'true' : 'false'"
|
||||
@click="includeNsfw = !includeNsfw"
|
||||
>
|
||||
<span class="wp-check" :data-checked="includeNsfw ? 'true' : 'false'" aria-hidden="true">
|
||||
<svg v-if="includeNsfw" viewBox="0 0 12 12" fill="none" style="display:block">
|
||||
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
Show 18+
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Tag rail -->
|
||||
@@ -232,12 +261,26 @@ onMounted(async () => {
|
||||
<style scoped>
|
||||
@import "../../community/community.css";
|
||||
|
||||
/* Filter-bar select widths — replaces ad-hoc `w-40` / `w-44` utility leaks
|
||||
with semantic names tied to the filter being constrained. */
|
||||
/* Filter-bar select widths. */
|
||||
.wp-comm-filter-select--kind { width: 160px; }
|
||||
.wp-comm-filter-select--sort { width: 180px; }
|
||||
|
||||
/* Empty-state CTA spacer — replaces the `mt-3` utility leak. */
|
||||
/* Toggle buttons — pill-shaped, cursor pointer, match prototype's wp-comm-toggle. */
|
||||
.wp-comm-filters__toggle {
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.wp-comm-filters__toggle:hover {
|
||||
background: var(--wp-bg-4);
|
||||
}
|
||||
.wp-comm-filters__toggle[data-active="true"] {
|
||||
background: color-mix(in oklab, var(--wp-accent-500) 16%, transparent);
|
||||
border-color: color-mix(in oklab, var(--wp-accent-500) 32%, transparent);
|
||||
color: var(--wp-accent-text);
|
||||
}
|
||||
|
||||
/* Empty-state CTA spacer. */
|
||||
.wp-empty__cta { margin-top: 12px; }
|
||||
|
||||
/* Loading skeleton fallback for the card hero gradient. */
|
||||
|
||||
@@ -11,7 +11,6 @@ import Button from "../../components/ui/Button.vue";
|
||||
import Input from "../../components/ui/Input.vue";
|
||||
import Textarea from "../../components/ui/Textarea.vue";
|
||||
import Select from "../../components/ui/Select.vue";
|
||||
import Checkbox from "../../components/ui/Checkbox.vue";
|
||||
import { useToast } from "../../composables/useToast";
|
||||
import { useCommunityStore } from "../../stores/communityStore";
|
||||
import { KIND_LABEL } from "../../community/format";
|
||||
@@ -259,9 +258,19 @@ function onTagKey(event: KeyboardEvent) {
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="wp-comm-filters__toggle">
|
||||
<Checkbox v-model="form.nsfw" /> Mark as 18+ (NSFW)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="wp-comm-filters__toggle"
|
||||
:data-active="form.nsfw ? 'true' : 'false'"
|
||||
@click="form.nsfw = !form.nsfw"
|
||||
>
|
||||
<span class="wp-check" :data-checked="form.nsfw ? 'true' : 'false'" aria-hidden="true">
|
||||
<svg v-if="form.nsfw" viewBox="0 0 12 12" fill="none" style="display:block">
|
||||
<path d="M3 6.2l2.2 2.2L9 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
Mark as 18+ (NSFW)
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-actions form-actions--split">
|
||||
<Button variant="secondary" icon="arrow-left" @click="step = 1">Back</Button>
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/wp/images/site.webmanifest" />
|
||||
<script type="module" crossorigin src="/wp/assets/manager-BWnFunYr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/wp/assets/manager-C6od6CgI.css">
|
||||
<script type="module" crossorigin src="/wp/assets/manager-Bs_mFjO4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/wp/assets/manager-CCg9HOCG.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user