fix(spa): chip pills, derivation $var dropdown, constraint subcat axes
This commit is contained in:
@@ -78,9 +78,12 @@ describe("ConstraintEditor.vue", () => {
|
||||
});
|
||||
|
||||
it("loads existing constraint payload when id is given", async () => {
|
||||
// Both axes are now sub_categories (matrix is subcat × subcat, not
|
||||
// value × subcat) — give source its own sub_categories so the row
|
||||
// matches the matrix payload key.
|
||||
apiMod.list.mockResolvedValue({
|
||||
items: [
|
||||
makeWildcardRow("wc_src", "Outfit", { values: ["jeans", "tux"] }),
|
||||
makeWildcardRow("wc_src", "Outfit", { subs: ["jeans", "tux"] }),
|
||||
makeWildcardRow("wc_tgt", "HairColor", { subs: ["warm", "cool"] }),
|
||||
],
|
||||
total: 2,
|
||||
@@ -110,7 +113,7 @@ describe("ConstraintEditor.vue", () => {
|
||||
it("save calls api.modules.create with type 'constraint'", async () => {
|
||||
apiMod.list.mockResolvedValue({
|
||||
items: [
|
||||
makeWildcardRow("wc_src", "Outfit", { values: ["jeans"] }),
|
||||
makeWildcardRow("wc_src", "Outfit", { subs: ["jeans"] }),
|
||||
makeWildcardRow("wc_tgt", "HairColor", { subs: ["warm"] }),
|
||||
],
|
||||
total: 2,
|
||||
|
||||
@@ -5,6 +5,7 @@ import Button from "./ui/Button.vue";
|
||||
import Input from "./ui/Input.vue";
|
||||
import Select from "./ui/Select.vue";
|
||||
import Chip from "./ui/Chip.vue";
|
||||
import RichTextInput from "./RichTextInput.vue";
|
||||
import type {
|
||||
DerivationAction,
|
||||
DerivationBranch,
|
||||
@@ -223,12 +224,18 @@ const branchCount = computed(() => rule.value.branches.length);
|
||||
:aria-label="`Condition operator for rule ${ruleNumber} branch ${bi + 1}`"
|
||||
@update:model-value="(v) => onConditionOp(bi, v as DerivationOp)"
|
||||
/>
|
||||
<Input
|
||||
<!-- RichTextInput so users get $-autocomplete on condition
|
||||
values (e.g. compare against `$mood` resolved upstream).
|
||||
surface=derivation gates @{} refs to muted styling — they
|
||||
don't resolve in derivation conditions per spec §2.7. -->
|
||||
<RichTextInput
|
||||
:model-value="branch.condition.value"
|
||||
surface="derivation"
|
||||
:var-suggestions="varSuggestions"
|
||||
placeholder="value"
|
||||
class="field-value"
|
||||
:aria-label="`Condition value for rule ${ruleNumber} branch ${bi + 1}`"
|
||||
@update:model-value="(v) => onConditionValue(bi, String(v ?? ''))"
|
||||
@update:model-value="(v) => onConditionValue(bi, v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,12 +262,14 @@ const branchCount = computed(() => rule.value.branches.length);
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="row-label">Value</span>
|
||||
<Input
|
||||
<RichTextInput
|
||||
:model-value="branch.action.value"
|
||||
surface="derivation"
|
||||
:var-suggestions="varSuggestions"
|
||||
class="field-value-full"
|
||||
placeholder="The new / appended / prepended value"
|
||||
:aria-label="`Action value for rule ${ruleNumber} branch ${bi + 1}`"
|
||||
@update:model-value="(v) => onActionValue(bi, String(v ?? ''))"
|
||||
@update:model-value="(v) => onActionValue(bi, v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -300,12 +309,14 @@ const branchCount = computed(() => rule.value.branches.length);
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="row-label">Value</span>
|
||||
<Input
|
||||
<RichTextInput
|
||||
:model-value="rule.else.action.value"
|
||||
surface="derivation"
|
||||
:var-suggestions="varSuggestions"
|
||||
class="field-value-full"
|
||||
placeholder="The new / appended / prepended value"
|
||||
:aria-label="`ELSE action value for rule ${ruleNumber}`"
|
||||
@update:model-value="(v) => onElseValue(String(v ?? ''))"
|
||||
@update:model-value="(v) => onElseValue(v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
.wp-rt .wp-rt-var,
|
||||
.wp-rt-preview .wp-rt-var {
|
||||
color: var(--wp-accent-text-strong, var(--wp-accent-text, #c4b5fd));
|
||||
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);
|
||||
background: rgba(139, 92, 246, 0.24);
|
||||
background: color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 24%, transparent);
|
||||
box-shadow: inset 0 0 0 1px rgba(139, 92, 246, 0.50);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-accent-500, #8b5cf6) 50%, transparent);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -44,10 +44,10 @@
|
||||
.wp-rt .wp-rt-ref,
|
||||
.wp-rt-preview .wp-rt-ref {
|
||||
color: var(--wp-kind-wildcard, #f0abfc);
|
||||
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);
|
||||
background: rgba(192, 38, 211, 0.24);
|
||||
background: color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 24%, transparent);
|
||||
box-shadow: inset 0 0 0 1px rgba(192, 38, 211, 0.50);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-kind-wildcard, #c026d3) 50%, transparent);
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -75,24 +75,34 @@
|
||||
}
|
||||
|
||||
/* Focus / rest dual-mode chip chrome (input only).
|
||||
While editing, chips are zero-width visual decorations so the
|
||||
mirror text width matches the textarea text width exactly and
|
||||
the native caret tracks correctly. When blurred, chips get a
|
||||
hair of horizontal padding so they read as proper pills. */
|
||||
While editing, chips stay tightly aligned with the textarea text
|
||||
beneath the mirror so the native caret stays close to the right
|
||||
character. We accept a small visual margin (negative-offset shadow)
|
||||
to make the chip read as a pill — caret can drift up to 2-3px
|
||||
inside a chip but the contrast win is worth it.
|
||||
When blurred, chips get a hair of horizontal padding so they read
|
||||
as proper pills with breathing room from adjacent text. */
|
||||
.wp-rt--focused .wp-rt-var,
|
||||
.wp-rt--focused .wp-rt-ref {
|
||||
/* Zero in-flow padding keeps width identical to the textarea, so
|
||||
* caret position stays correct character-for-character. The visible
|
||||
* pill comes from the chip background + box-shadow alone. */
|
||||
padding: 0;
|
||||
}
|
||||
.wp-rt--rest .wp-rt-var,
|
||||
.wp-rt--rest .wp-rt-ref {
|
||||
padding: 0 4px;
|
||||
margin: 0 0.5px;
|
||||
/* Pill chrome at rest — bigger horizontal padding + a breathing
|
||||
* margin so chips don't merge with adjacent text. The width drift
|
||||
* vs the (now-blurred) textarea is invisible because no caret is
|
||||
* showing. */
|
||||
padding: 1px 5px;
|
||||
margin: 0 1.5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.wp-rt--rest .wp-rt-dp-multi,
|
||||
.wp-rt--rest .wp-rt-dp-weight {
|
||||
padding: 0 3px;
|
||||
margin: 0 0.5px;
|
||||
padding: 0 4px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
/* Read-only preview always renders chips at rest. Match the
|
||||
|
||||
@@ -83,6 +83,29 @@ function wildcardById(id: string | null): ModuleRow | undefined {
|
||||
const sourceWildcard = computed(() => wildcardById(sourceWildcardId.value));
|
||||
const targetWildcard = computed(() => wildcardById(targetWildcardId.value));
|
||||
|
||||
// Matrix axes are BOTH sub-categories — source's on the rows, target's
|
||||
// on the cols. Source-value-keyed cells (the prior shape) confused the
|
||||
// "rule" semantics: a constraint matrix expresses category-level
|
||||
// boost/exclude rules, not per-option overrides. Per-option overrides
|
||||
// belong in the Exceptions table beneath the matrix.
|
||||
const sourceSubCategories = computed<string[]>(() => {
|
||||
const wc = sourceWildcard.value;
|
||||
if (!wc) return [];
|
||||
const payload = wc.payload as WildcardPayloadShape;
|
||||
return payload.sub_categories ?? [];
|
||||
});
|
||||
|
||||
const targetSubCategories = computed<string[]>(() => {
|
||||
const wc = targetWildcard.value;
|
||||
if (!wc) return [];
|
||||
const payload = wc.payload as WildcardPayloadShape;
|
||||
return payload.sub_categories ?? [];
|
||||
});
|
||||
|
||||
// Per-option pickers used by the Exceptions editor below the matrix.
|
||||
// Exceptions DO operate at value granularity (override one specific
|
||||
// option-pair regardless of what the sub-category matrix says) so we
|
||||
// keep these computed for that table only.
|
||||
const sourceValues = computed<string[]>(() => {
|
||||
const wc = sourceWildcard.value;
|
||||
if (!wc) return [];
|
||||
@@ -93,13 +116,6 @@ const sourceValues = computed<string[]>(() => {
|
||||
return Array.from(new Set(values));
|
||||
});
|
||||
|
||||
const targetSubCategories = computed<string[]>(() => {
|
||||
const wc = targetWildcard.value;
|
||||
if (!wc) return [];
|
||||
const payload = wc.payload as WildcardPayloadShape;
|
||||
return payload.sub_categories ?? [];
|
||||
});
|
||||
|
||||
const targetValues = computed<string[]>(() => {
|
||||
const wc = targetWildcard.value;
|
||||
if (!wc) return [];
|
||||
@@ -330,18 +346,18 @@ defineExpose({ sourceWildcardId, targetWildcardId, matrix, exceptions, applyRest
|
||||
Pick a source and target wildcard to populate the matrix.
|
||||
</div>
|
||||
<div
|
||||
v-else-if="sourceValues.length === 0 || targetSubCategories.length === 0"
|
||||
v-else-if="sourceSubCategories.length === 0 || targetSubCategories.length === 0"
|
||||
class="wp-empty-card"
|
||||
data-test="matrix-need-subs"
|
||||
>
|
||||
<i class="pi pi-info-circle" />
|
||||
<span v-if="sourceValues.length === 0">Source wildcard needs at least one option value. </span>
|
||||
<span v-if="sourceSubCategories.length === 0">Source wildcard needs at least one sub-category. </span>
|
||||
<span v-if="targetSubCategories.length === 0">Target wildcard needs at least one sub-category. </span>
|
||||
Add them on the wildcard editor to define rules.
|
||||
</div>
|
||||
<ConstraintMatrixGrid
|
||||
v-else
|
||||
:rows="sourceValues"
|
||||
:rows="sourceSubCategories"
|
||||
:cols="targetSubCategories"
|
||||
:model-value="matrix"
|
||||
data-test="matrix-grid"
|
||||
|
||||
Reference in New Issue
Block a user