@dumiflex/wp-kit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +146 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +293 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +98 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/loading-mark-motion.ts +249 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
|
@@ -1,708 +1,708 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Read-only grouped-axis constraint matrix preview.
|
|
4
|
-
*
|
|
5
|
-
* Ported from the sister project's interactive `ConstraintMatrix.vue` with all
|
|
6
|
-
* editing interaction stripped: no click/keydown handlers, no CellRulePopover,
|
|
7
|
-
* no Teleport, no sticky-band measurement, no defineExpose, no emit.
|
|
8
|
-
*
|
|
9
|
-
* Two rendering paths:
|
|
10
|
-
* - Grouped: when `targetGroups` or `sourceGroups` are non-empty the grid
|
|
11
|
-
* grows column-axis bands, per-axis row header chips, and a labelled
|
|
12
|
-
* "uncategorized" bucket. The `groupHue`/`groupLabel`/`isBucket` logic
|
|
13
|
-
* comes from `matrix-axis.ts` (Task 2, same repo).
|
|
14
|
-
* - Flat: when both group maps are empty the grid is the community's legacy
|
|
15
|
-
* single-row header layout (back-compat for old payloads that predate axis
|
|
16
|
-
* grouping). Source/target tint applied via CSS vars.
|
|
17
|
-
*
|
|
18
|
-
* Exceptions use `TokenizedValue` so `$var` / `@{ref}` tokens render as
|
|
19
|
-
* coloured chips instead of raw strings. No `v-html`.
|
|
20
|
-
*
|
|
21
|
-
* CSS uses `.wp-cn-*` prefix (community namespace) throughout, with band/chip/
|
|
22
|
-
* eyebrow rules ported from the sister `.wp-mx-*` rules and renamed.
|
|
23
|
-
* Sticky frozen headers are omitted in favour of a simple `overflow-x: auto`
|
|
24
|
-
* wrapper — correctness before complexity for a read-only preview.
|
|
25
|
-
*/
|
|
26
|
-
import { computed } from "vue";
|
|
27
|
-
import TokenizedValue from "./TokenizedValue.vue";
|
|
28
|
-
import {
|
|
29
|
-
groupHue as tagHue,
|
|
30
|
-
groupLabel,
|
|
31
|
-
isBucket,
|
|
32
|
-
orderByGroups,
|
|
33
|
-
toGroups,
|
|
34
|
-
} from "./matrix-axis";
|
|
35
|
-
|
|
36
|
-
const props = defineProps<{
|
|
37
|
-
rows: string[];
|
|
38
|
-
cols: string[];
|
|
39
|
-
matrix: Record<string, Record<string, { mode: string; factor: number }>>;
|
|
40
|
-
sourceName: string;
|
|
41
|
-
targetName: string;
|
|
42
|
-
sourceGroups: Record<string, string[]>;
|
|
43
|
-
targetGroups: Record<string, string[]>;
|
|
44
|
-
exceptions: { source: string; target: string; mode: string; factor: number }[];
|
|
45
|
-
targetSelect: string | null;
|
|
46
|
-
refMap: ReadonlyMap<string, string>;
|
|
47
|
-
}>();
|
|
48
|
-
|
|
49
|
-
const orderedRows = computed(() => orderByGroups(props.rows, props.sourceGroups));
|
|
50
|
-
const orderedCols = computed(() => orderByGroups(props.cols, props.targetGroups));
|
|
51
|
-
const rowGroups = computed(() => toGroups(orderedRows.value));
|
|
52
|
-
const colGroups = computed(() => toGroups(orderedCols.value));
|
|
53
|
-
const hasColBands = computed(() => colGroups.value.some((g) => !isBucket(g)));
|
|
54
|
-
const hasRowGroups = computed(() => rowGroups.value.some((g) => !isBucket(g)));
|
|
55
|
-
|
|
56
|
-
// `only` (schema v6, linked picks): when the source fires, targets without a
|
|
57
|
-
// rule of their own in that row are excluded.
|
|
58
|
-
type CnMode = "allow" | "boost" | "reduce" | "exclude" | "only";
|
|
59
|
-
const MODE_GLYPH: Record<string, string> = {
|
|
60
|
-
only: "✓",
|
|
61
|
-
boost: "↑",
|
|
62
|
-
reduce: "↓",
|
|
63
|
-
exclude: "×",
|
|
64
|
-
allow: "·",
|
|
65
|
-
};
|
|
66
|
-
const MODE_LABEL: Record<CnMode, string> = {
|
|
67
|
-
boost: "Boost",
|
|
68
|
-
reduce: "Reduce",
|
|
69
|
-
exclude: "Exclude",
|
|
70
|
-
allow: "Neutral",
|
|
71
|
-
only: "Only",
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function normalizeMode(raw: string | null | undefined): CnMode {
|
|
75
|
-
return raw === "exclude" || raw === "boost" || raw === "reduce" || raw === "only" ? raw : "allow";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A row holding an `only` cell shuts out its neutral cells at runtime; the
|
|
79
|
-
* preview marks them so the link reads the same as in the extension. */
|
|
80
|
-
function isImpliedOut(row: string, col: string): boolean {
|
|
81
|
-
if (cellAt(row, col).mode !== "allow") return false;
|
|
82
|
-
return Object.values(props.matrix?.[row] ?? {}).some((c) => c?.mode === "only");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function cellAt(row: string, col: string): { mode: CnMode; factor: number } {
|
|
86
|
-
const c = props.matrix?.[row]?.[col];
|
|
87
|
-
return {
|
|
88
|
-
mode: normalizeMode(c?.mode),
|
|
89
|
-
factor: typeof c?.factor === "number" ? c.factor : 1,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function fmtFactor(f: number): string {
|
|
94
|
-
if (!Number.isFinite(f)) return "1";
|
|
95
|
-
if (f >= 10) return f.toFixed(0);
|
|
96
|
-
return f.toFixed(2).replace(/\.?0+$/, "");
|
|
97
|
-
}
|
|
98
|
-
</script>
|
|
99
|
-
|
|
100
|
-
<template>
|
|
101
|
-
<div class="wp-cn">
|
|
102
|
-
<!-- Axis header -->
|
|
103
|
-
<div class="wp-cn-head">
|
|
104
|
-
<span
|
|
105
|
-
class="wp-cn-axis wp-cn-axis--src"
|
|
106
|
-
data-test="cn-axis-src"
|
|
107
|
-
>
|
|
108
|
-
{{ sourceName }}
|
|
109
|
-
</span>
|
|
110
|
-
<i class="pi pi-arrow-right wp-cn-head__arrow" />
|
|
111
|
-
<span
|
|
112
|
-
class="wp-cn-axis wp-cn-axis--tgt"
|
|
113
|
-
data-test="cn-axis-tgt"
|
|
114
|
-
>
|
|
115
|
-
{{ targetName }}
|
|
116
|
-
</span>
|
|
117
|
-
<span
|
|
118
|
-
v-if="targetSelect"
|
|
119
|
-
class="wp-cn-target-select"
|
|
120
|
-
title="Library reach selector — which downstream target instances this constraint covers"
|
|
121
|
-
>
|
|
122
|
-
target: {{ targetSelect }}
|
|
123
|
-
</span>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<!-- Matrix grid -->
|
|
127
|
-
<div
|
|
128
|
-
v-if="rows.length"
|
|
129
|
-
class="wp-cn-grid-wrap"
|
|
130
|
-
>
|
|
131
|
-
<table class="wp-cn-table">
|
|
132
|
-
<thead>
|
|
133
|
-
<!-- Band row: axis names spanning their tags (grouped path only) -->
|
|
134
|
-
<tr>
|
|
135
|
-
<th
|
|
136
|
-
class="wp-cn-corner"
|
|
137
|
-
:rowspan="hasColBands ? 2 : 1"
|
|
138
|
-
>
|
|
139
|
-
<span class="chip">
|
|
140
|
-
<span class="row-1">↓ source</span>
|
|
141
|
-
<span class="row-2">→ target</span>
|
|
142
|
-
</span>
|
|
143
|
-
</th>
|
|
144
|
-
<template v-if="hasColBands">
|
|
145
|
-
<th
|
|
146
|
-
v-for="(grp, gi) in colGroups"
|
|
147
|
-
:key="`band-${gi}`"
|
|
148
|
-
class="wp-cn-band"
|
|
149
|
-
:class="{ 'wp-cn-band--bucket': isBucket(grp) }"
|
|
150
|
-
:colspan="grp.tags.length"
|
|
151
|
-
:style="{ '--ax': tagHue(grp, 'target', true) }"
|
|
152
|
-
:title="groupLabel(grp)"
|
|
153
|
-
>
|
|
154
|
-
<span class="chip">{{ groupLabel(grp) }}</span>
|
|
155
|
-
</th>
|
|
156
|
-
</template>
|
|
157
|
-
<!-- Flat path: single tag header row -->
|
|
158
|
-
<template v-else>
|
|
159
|
-
<th
|
|
160
|
-
v-for="c in orderedCols"
|
|
161
|
-
:key="c.tag"
|
|
162
|
-
class="wp-cn-colhead"
|
|
163
|
-
:style="{ '--ax': 'var(--wp-constraint-target)' }"
|
|
164
|
-
>
|
|
165
|
-
<span class="chip">{{ c.tag }}</span>
|
|
166
|
-
</th>
|
|
167
|
-
</template>
|
|
168
|
-
</tr>
|
|
169
|
-
<!-- Tag row (grouped path only): per-tag headers below the bands -->
|
|
170
|
-
<tr v-if="hasColBands">
|
|
171
|
-
<template
|
|
172
|
-
v-for="(grp, gi) in colGroups"
|
|
173
|
-
:key="`coltags-${gi}`"
|
|
174
|
-
>
|
|
175
|
-
<th
|
|
176
|
-
v-for="(tag, ti) in grp.tags"
|
|
177
|
-
:key="tag"
|
|
178
|
-
class="wp-cn-colhead"
|
|
179
|
-
:class="{ 'group-start': !isBucket(grp) && ti === 0 }"
|
|
180
|
-
:style="{ '--ax': tagHue(grp, 'target', hasColBands) }"
|
|
181
|
-
>
|
|
182
|
-
<span class="chip">{{ tag }}</span>
|
|
183
|
-
</th>
|
|
184
|
-
</template>
|
|
185
|
-
</tr>
|
|
186
|
-
</thead>
|
|
187
|
-
<tbody>
|
|
188
|
-
<template
|
|
189
|
-
v-for="(grp, gi) in rowGroups"
|
|
190
|
-
:key="`rowgrp-${gi}`"
|
|
191
|
-
>
|
|
192
|
-
<!-- Group header chip: named multi-tag axis OR labelled uncategorized bucket -->
|
|
193
|
-
<tr
|
|
194
|
-
v-if="(!isBucket(grp) && grp.tags.length > 1) || (isBucket(grp) && hasRowGroups)"
|
|
195
|
-
class="wp-cn-grp-row"
|
|
196
|
-
>
|
|
197
|
-
<th
|
|
198
|
-
class="wp-cn-grp-head"
|
|
199
|
-
:class="{ 'wp-cn-grp-head--bucket': isBucket(grp) }"
|
|
200
|
-
:colspan="orderedCols.length + 1"
|
|
201
|
-
:style="{ '--ax': tagHue(grp, 'source', true) }"
|
|
202
|
-
:title="groupLabel(grp)"
|
|
203
|
-
>
|
|
204
|
-
<span class="chip">{{ groupLabel(grp) }}</span>
|
|
205
|
-
</th>
|
|
206
|
-
</tr>
|
|
207
|
-
<!-- Tag rows -->
|
|
208
|
-
<tr
|
|
209
|
-
v-for="tag in grp.tags"
|
|
210
|
-
:key="tag"
|
|
211
|
-
>
|
|
212
|
-
<th
|
|
213
|
-
class="wp-cn-rowhead"
|
|
214
|
-
:class="{
|
|
215
|
-
'group-start': !isBucket(grp) && tag === grp.tags[0],
|
|
216
|
-
solo: !isBucket(grp) && grp.tags.length === 1,
|
|
217
|
-
}"
|
|
218
|
-
:style="{ '--ax': tagHue(grp, 'source', hasRowGroups) }"
|
|
219
|
-
>
|
|
220
|
-
<!-- Solo named axis: fold axis name in as an eyebrow above the tag -->
|
|
221
|
-
<span
|
|
222
|
-
v-if="!isBucket(grp) && grp.tags.length === 1"
|
|
223
|
-
class="chip"
|
|
224
|
-
>
|
|
225
|
-
<span class="eye">{{ grp.axisName }}</span>
|
|
226
|
-
<span class="v">{{ tag }}</span>
|
|
227
|
-
</span>
|
|
228
|
-
<span
|
|
229
|
-
v-else
|
|
230
|
-
class="chip"
|
|
231
|
-
>{{ tag }}</span>
|
|
232
|
-
</th>
|
|
233
|
-
<td
|
|
234
|
-
v-for="c in orderedCols"
|
|
235
|
-
:key="c.tag"
|
|
236
|
-
class="wp-cn-td"
|
|
237
|
-
>
|
|
238
|
-
<span
|
|
239
|
-
class="wp-cn-cell"
|
|
240
|
-
:class="{ 'wp-cn-cell--implied-out': isImpliedOut(tag, c.tag) }"
|
|
241
|
-
:data-mode="cellAt(tag, c.tag).mode"
|
|
242
|
-
>
|
|
243
|
-
<span class="wp-cn-cell__glyph">{{ isImpliedOut(tag, c.tag) ? "×" : MODE_GLYPH[cellAt(tag, c.tag).mode] }}</span>
|
|
244
|
-
<span
|
|
245
|
-
v-if="cellAt(tag, c.tag).mode === 'boost' || cellAt(tag, c.tag).mode === 'reduce'"
|
|
246
|
-
class="wp-cn-cell__factor"
|
|
247
|
-
>×{{ fmtFactor(cellAt(tag, c.tag).factor) }}</span>
|
|
248
|
-
</span>
|
|
249
|
-
</td>
|
|
250
|
-
</tr>
|
|
251
|
-
</template>
|
|
252
|
-
</tbody>
|
|
253
|
-
</table>
|
|
254
|
-
</div>
|
|
255
|
-
|
|
256
|
-
<!-- Legend -->
|
|
257
|
-
<div class="wp-cn-legend">
|
|
258
|
-
<span><b data-mode="boost">↑</b> boost</span>
|
|
259
|
-
<span><b data-mode="reduce">↓</b> reduce</span>
|
|
260
|
-
<span><b data-mode="exclude">×</b> exclude</span>
|
|
261
|
-
<span><b data-mode="neutral">·</b> neutral</span>
|
|
262
|
-
<span><b data-mode="only">✓</b> only (links picks)</span>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<!-- Exceptions (library-only) -->
|
|
266
|
-
<div
|
|
267
|
-
v-if="exceptions.length"
|
|
268
|
-
class="wp-cn-exceptions"
|
|
269
|
-
>
|
|
270
|
-
<div class="wp-cn-exceptions__label">
|
|
271
|
-
Exceptions
|
|
272
|
-
</div>
|
|
273
|
-
<div
|
|
274
|
-
v-for="(exc, i) in exceptions"
|
|
275
|
-
:key="`exc-${i}`"
|
|
276
|
-
class="wp-cn-exc-row"
|
|
277
|
-
:class="`wp-cn-exc-row--${exc.mode}`"
|
|
278
|
-
:data-test="`cn-exc-${i}`"
|
|
279
|
-
>
|
|
280
|
-
<span class="wp-cn-exc__pair">
|
|
281
|
-
<span class="wp-cn-exc__src">
|
|
282
|
-
<TokenizedValue
|
|
283
|
-
:value="exc.source"
|
|
284
|
-
:uuid-to-name="refMap"
|
|
285
|
-
/>
|
|
286
|
-
</span>
|
|
287
|
-
<i class="pi pi-arrow-right wp-cn-exc__arrow" />
|
|
288
|
-
<span class="wp-cn-exc__tgt">
|
|
289
|
-
<TokenizedValue
|
|
290
|
-
:value="exc.target"
|
|
291
|
-
:uuid-to-name="refMap"
|
|
292
|
-
/>
|
|
293
|
-
</span>
|
|
294
|
-
</span>
|
|
295
|
-
<span
|
|
296
|
-
class="wp-cn-exc__mode"
|
|
297
|
-
:class="`wp-cn-exc__mode--${exc.mode}`"
|
|
298
|
-
>
|
|
299
|
-
<span class="wp-cn-exc__glyph">{{ MODE_GLYPH[exc.mode] }}</span>
|
|
300
|
-
{{ MODE_LABEL[normalizeMode(exc.mode)] }}
|
|
301
|
-
</span>
|
|
302
|
-
<span
|
|
303
|
-
v-if="exc.mode === 'boost' || exc.mode === 'reduce'"
|
|
304
|
-
class="wp-cn-exc__factor"
|
|
305
|
-
>×{{ fmtFactor(exc.factor) }}</span>
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</template>
|
|
310
|
-
|
|
311
|
-
<style scoped>
|
|
312
|
-
/* ── Container ─────────────────────────────────────────────── */
|
|
313
|
-
.wp-cn {
|
|
314
|
-
display: flex;
|
|
315
|
-
flex-direction: column;
|
|
316
|
-
gap: 14px;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/* ── Axis header ───────────────────────────────────────────── */
|
|
320
|
-
.wp-cn-head {
|
|
321
|
-
display: flex;
|
|
322
|
-
align-items: center;
|
|
323
|
-
gap: 8px;
|
|
324
|
-
flex-wrap: wrap;
|
|
325
|
-
}
|
|
326
|
-
.wp-cn-head__arrow {
|
|
327
|
-
font-size: 11px;
|
|
328
|
-
color: var(--wp-text-dim);
|
|
329
|
-
}
|
|
330
|
-
.wp-cn-axis {
|
|
331
|
-
display: inline-flex;
|
|
332
|
-
align-items: center;
|
|
333
|
-
padding: 3px 9px;
|
|
334
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
335
|
-
font-size: 11px;
|
|
336
|
-
font-weight: 600;
|
|
337
|
-
letter-spacing: 0.02em;
|
|
338
|
-
white-space: nowrap;
|
|
339
|
-
}
|
|
340
|
-
.wp-cn-axis--src {
|
|
341
|
-
color: var(--wp-constraint-source-text);
|
|
342
|
-
background: color-mix(in oklab, var(--wp-constraint-source) 14%, transparent);
|
|
343
|
-
border: 1px solid color-mix(in oklab, var(--wp-constraint-source) 35%, transparent);
|
|
344
|
-
}
|
|
345
|
-
.wp-cn-axis--tgt {
|
|
346
|
-
color: var(--wp-constraint-target-text);
|
|
347
|
-
background: color-mix(in oklab, var(--wp-constraint-target) 14%, transparent);
|
|
348
|
-
border: 1px solid color-mix(in oklab, var(--wp-constraint-target) 35%, transparent);
|
|
349
|
-
}
|
|
350
|
-
.wp-cn-target-select {
|
|
351
|
-
font-size: 10.5px;
|
|
352
|
-
color: var(--wp-text-muted);
|
|
353
|
-
background: var(--wp-bg-3);
|
|
354
|
-
padding: 2px 8px;
|
|
355
|
-
border-radius: var(--wp-radius-cap, 999px);
|
|
356
|
-
font-family: var(--wp-font-mono);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/* ── Grid wrapper (simple scroll, no sticky headers) ───────── */
|
|
360
|
-
/* Frame hugs the table (align-self instead of stretching to the card width)
|
|
361
|
-
and scrolls on BOTH axes for wide/tall matrices — mirrors the sister's
|
|
362
|
-
inline-block grid. */
|
|
363
|
-
.wp-cn-grid-wrap {
|
|
364
|
-
align-self: flex-start;
|
|
365
|
-
max-width: 100%;
|
|
366
|
-
overflow: auto;
|
|
367
|
-
background: var(--wp-bg-1);
|
|
368
|
-
border: 1px solid var(--wp-border);
|
|
369
|
-
border-radius: min(6px, var(--wp-radius-cap, 999px));
|
|
370
|
-
padding: 0;
|
|
371
|
-
}
|
|
372
|
-
/* `separate` + zero spacing; the 4px inter-cell gap comes from th/td padding,
|
|
373
|
-
so the columns the <th> headers form line up exactly with the body <td>s. */
|
|
374
|
-
.wp-cn-table {
|
|
375
|
-
border-collapse: separate;
|
|
376
|
-
border-spacing: 0;
|
|
377
|
-
}
|
|
378
|
-
.wp-cn-table th,
|
|
379
|
-
.wp-cn-table td {
|
|
380
|
-
vertical-align: middle;
|
|
381
|
-
padding: 2px;
|
|
382
|
-
}
|
|
383
|
-
/* The <td> stays a real table cell (so columns align); the inner
|
|
384
|
-
.wp-cn-cell carries the fixed width + flex layout. Putting flex on the
|
|
385
|
-
<td> itself pulls it out of the table formatting context and breaks
|
|
386
|
-
alignment. */
|
|
387
|
-
.wp-cn-td {
|
|
388
|
-
position: relative;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/* ── Corner chip ───────────────────────────────────────────── */
|
|
392
|
-
.wp-cn-corner {
|
|
393
|
-
background: var(--wp-bg-2);
|
|
394
|
-
}
|
|
395
|
-
.wp-cn-corner .chip {
|
|
396
|
-
display: flex;
|
|
397
|
-
flex-direction: column;
|
|
398
|
-
justify-content: center;
|
|
399
|
-
width: 96px;
|
|
400
|
-
min-height: 36px;
|
|
401
|
-
height: 100%;
|
|
402
|
-
padding: 5px 8px;
|
|
403
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
404
|
-
font-family: var(--wp-font-mono);
|
|
405
|
-
font-size: 9px;
|
|
406
|
-
line-height: 1.3;
|
|
407
|
-
text-align: left;
|
|
408
|
-
background: color-mix(in srgb, var(--wp-text) 4%, var(--wp-bg-2));
|
|
409
|
-
}
|
|
410
|
-
.wp-cn-corner .row-1 {
|
|
411
|
-
color: var(--wp-constraint-source-text);
|
|
412
|
-
}
|
|
413
|
-
.wp-cn-corner .row-2 {
|
|
414
|
-
color: var(--wp-constraint-target-text);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/* ── Column band — axis name spanning its tags ─────────────── */
|
|
418
|
-
.wp-cn-band .chip {
|
|
419
|
-
display: flex;
|
|
420
|
-
align-items: center;
|
|
421
|
-
justify-content: center;
|
|
422
|
-
height: 22px;
|
|
423
|
-
padding: 0 8px;
|
|
424
|
-
font: 700 9px var(--wp-font-sans, sans-serif);
|
|
425
|
-
letter-spacing: 0.06em;
|
|
426
|
-
text-transform: uppercase;
|
|
427
|
-
white-space: nowrap;
|
|
428
|
-
overflow: hidden;
|
|
429
|
-
text-overflow: ellipsis;
|
|
430
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0 0;
|
|
431
|
-
color: color-mix(in srgb, var(--ax) 82%, var(--wp-text));
|
|
432
|
-
background: color-mix(in srgb, var(--ax) 15%, var(--wp-bg-2));
|
|
433
|
-
border-bottom: 2px solid color-mix(in srgb, var(--ax) 55%, transparent);
|
|
434
|
-
}
|
|
435
|
-
/* Uncategorized bucket: dashed edge instead of solid axis accent */
|
|
436
|
-
.wp-cn-band--bucket .chip {
|
|
437
|
-
border-bottom-style: dashed;
|
|
438
|
-
}
|
|
439
|
-
.wp-cn-grp-head--bucket .chip {
|
|
440
|
-
border-left-style: dashed;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* ── Column tag chip ───────────────────────────────────────── */
|
|
444
|
-
.wp-cn-colhead .chip {
|
|
445
|
-
display: flex;
|
|
446
|
-
align-items: center;
|
|
447
|
-
justify-content: center;
|
|
448
|
-
width: 64px;
|
|
449
|
-
min-height: 28px;
|
|
450
|
-
padding: 3px 2px;
|
|
451
|
-
font: 700 10px var(--wp-font-sans, sans-serif);
|
|
452
|
-
border-radius: 0 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px));
|
|
453
|
-
color: color-mix(in srgb, var(--ax) 72%, var(--wp-text));
|
|
454
|
-
background: color-mix(in srgb, var(--ax) 9%, var(--wp-bg-2));
|
|
455
|
-
}
|
|
456
|
-
.wp-cn-colhead.group-start .chip {
|
|
457
|
-
border-left: 2px solid color-mix(in srgb, var(--ax) 45%, transparent);
|
|
458
|
-
}
|
|
459
|
-
/* Flat path: col header with the default target tint */
|
|
460
|
-
.wp-cn-colhead {
|
|
461
|
-
color: var(--wp-constraint-target-text);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/* ── Row group header chip — names the axis above its tags ─── */
|
|
465
|
-
.wp-cn-grp-row .wp-cn-grp-head {
|
|
466
|
-
text-align: left;
|
|
467
|
-
padding: 4px 0 1px;
|
|
468
|
-
}
|
|
469
|
-
.wp-cn-grp-head .chip {
|
|
470
|
-
display: inline-flex;
|
|
471
|
-
align-items: center;
|
|
472
|
-
max-width: 220px;
|
|
473
|
-
height: 22px;
|
|
474
|
-
padding: 0 11px;
|
|
475
|
-
font: 700 9px var(--wp-font-sans, sans-serif);
|
|
476
|
-
letter-spacing: 0.1em;
|
|
477
|
-
text-transform: uppercase;
|
|
478
|
-
white-space: nowrap;
|
|
479
|
-
overflow: hidden;
|
|
480
|
-
text-overflow: ellipsis;
|
|
481
|
-
border-radius: 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0;
|
|
482
|
-
color: color-mix(in srgb, var(--ax) 80%, var(--wp-text));
|
|
483
|
-
background: color-mix(in srgb, var(--ax) 16%, var(--wp-bg-2));
|
|
484
|
-
border-left: 3px solid var(--ax);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/* ── Row tag chip ──────────────────────────────────────────── */
|
|
488
|
-
.wp-cn-rowhead .chip {
|
|
489
|
-
display: flex;
|
|
490
|
-
align-items: center;
|
|
491
|
-
justify-content: flex-start;
|
|
492
|
-
width: 96px;
|
|
493
|
-
height: 36px;
|
|
494
|
-
padding: 0 10px;
|
|
495
|
-
font: 700 10px var(--wp-font-sans, sans-serif);
|
|
496
|
-
text-align: left;
|
|
497
|
-
border-radius: 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0;
|
|
498
|
-
color: color-mix(in srgb, var(--ax) 76%, var(--wp-text));
|
|
499
|
-
background: color-mix(in srgb, var(--ax) 10%, var(--wp-bg-2));
|
|
500
|
-
border-left: 2px solid color-mix(in srgb, var(--ax) 48%, transparent);
|
|
501
|
-
}
|
|
502
|
-
/* Solo named axis — eyebrow + value stacked */
|
|
503
|
-
.wp-cn-rowhead.solo .chip {
|
|
504
|
-
flex-direction: column;
|
|
505
|
-
align-items: flex-start;
|
|
506
|
-
justify-content: center;
|
|
507
|
-
gap: 2px;
|
|
508
|
-
border-left-width: 3px;
|
|
509
|
-
border-left-color: var(--ax);
|
|
510
|
-
}
|
|
511
|
-
.wp-cn-rowhead.solo .eye {
|
|
512
|
-
max-width: 78px;
|
|
513
|
-
overflow: hidden;
|
|
514
|
-
text-overflow: ellipsis;
|
|
515
|
-
white-space: nowrap;
|
|
516
|
-
font: 700 7px var(--wp-font-sans, sans-serif);
|
|
517
|
-
letter-spacing: 0.08em;
|
|
518
|
-
text-transform: uppercase;
|
|
519
|
-
color: color-mix(in srgb, var(--ax) 72%, var(--wp-text-dim));
|
|
520
|
-
}
|
|
521
|
-
.wp-cn-rowhead.solo .v {
|
|
522
|
-
font: 600 11px var(--wp-font-sans, sans-serif);
|
|
523
|
-
color: var(--wp-text);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/* ── Matrix cells ──────────────────────────────────────────── */
|
|
527
|
-
/* Inner cell box — fixed 64×36 so every column lines up; read-only (no
|
|
528
|
-
cursor, no hover lift). */
|
|
529
|
-
.wp-cn-cell {
|
|
530
|
-
width: 64px;
|
|
531
|
-
height: 36px;
|
|
532
|
-
box-sizing: border-box;
|
|
533
|
-
display: inline-flex;
|
|
534
|
-
align-items: center;
|
|
535
|
-
justify-content: center;
|
|
536
|
-
gap: 4px;
|
|
537
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
538
|
-
font-family: var(--wp-font-mono);
|
|
539
|
-
font-size: 12px;
|
|
540
|
-
font-weight: 600;
|
|
541
|
-
}
|
|
542
|
-
.wp-cn-cell__glyph {
|
|
543
|
-
font-size: 13px;
|
|
544
|
-
line-height: 1;
|
|
545
|
-
}
|
|
546
|
-
.wp-cn-cell__factor {
|
|
547
|
-
font-size: 10.5px;
|
|
548
|
-
font-weight: 700;
|
|
549
|
-
}
|
|
550
|
-
.wp-cn-cell[data-mode="neutral"],
|
|
551
|
-
.wp-cn-cell[data-mode="allow"] {
|
|
552
|
-
color: var(--wp-text-dim);
|
|
553
|
-
background: color-mix(in srgb, var(--wp-text) 5%, transparent);
|
|
554
|
-
border: 1px dashed color-mix(in srgb, var(--wp-text) 10%, transparent);
|
|
555
|
-
}
|
|
556
|
-
.wp-cn-cell[data-mode="boost"] {
|
|
557
|
-
color: var(--wp-success);
|
|
558
|
-
background: color-mix(in srgb, var(--wp-success) 22%, transparent);
|
|
559
|
-
border: 1px solid color-mix(in srgb, var(--wp-success) 45%, transparent);
|
|
560
|
-
}
|
|
561
|
-
.wp-cn-cell[data-mode="reduce"] {
|
|
562
|
-
color: var(--wp-warn);
|
|
563
|
-
background: color-mix(in srgb, var(--wp-warn) 22%, transparent);
|
|
564
|
-
border: 1px solid color-mix(in srgb, var(--wp-warn) 45%, transparent);
|
|
565
|
-
}
|
|
566
|
-
.wp-cn-cell[data-mode="only"] {
|
|
567
|
-
color: var(--wp-info);
|
|
568
|
-
background: color-mix(in srgb, var(--wp-info) 22%, transparent);
|
|
569
|
-
border: 1px solid color-mix(in srgb, var(--wp-info) 45%, transparent);
|
|
570
|
-
}
|
|
571
|
-
.wp-cn-cell.wp-cn-cell--implied-out {
|
|
572
|
-
color: color-mix(in srgb, var(--wp-danger) 60%, var(--wp-text-dim));
|
|
573
|
-
border-color: color-mix(in srgb, var(--wp-danger) 30%, transparent);
|
|
574
|
-
}
|
|
575
|
-
.wp-cn-cell[data-mode="exclude"] {
|
|
576
|
-
color: var(--wp-danger);
|
|
577
|
-
background: color-mix(in srgb, var(--wp-danger) 22%, transparent);
|
|
578
|
-
border: 1px solid color-mix(in srgb, var(--wp-danger) 45%, transparent);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/* ── Legend ────────────────────────────────────────────────── */
|
|
582
|
-
.wp-cn-legend {
|
|
583
|
-
display: flex;
|
|
584
|
-
flex-wrap: wrap;
|
|
585
|
-
gap: 14px;
|
|
586
|
-
font-size: 11px;
|
|
587
|
-
color: var(--wp-text-muted);
|
|
588
|
-
}
|
|
589
|
-
.wp-cn-legend b {
|
|
590
|
-
font-family: var(--wp-font-mono);
|
|
591
|
-
color: var(--wp-text);
|
|
592
|
-
}
|
|
593
|
-
.wp-cn-legend b[data-mode="boost"] { color: var(--wp-success); }
|
|
594
|
-
.wp-cn-legend b[data-mode="reduce"] { color: var(--wp-warn); }
|
|
595
|
-
.wp-cn-legend b[data-mode="exclude"] { color: var(--wp-danger); }
|
|
596
|
-
.wp-cn-legend b[data-mode="neutral"] { color: var(--wp-text-dim); }
|
|
597
|
-
.wp-cn-legend b[data-mode="only"] { color: var(--wp-info); }
|
|
598
|
-
|
|
599
|
-
/* ── Exceptions ────────────────────────────────────────────── */
|
|
600
|
-
.wp-cn-exceptions {
|
|
601
|
-
display: flex;
|
|
602
|
-
flex-direction: column;
|
|
603
|
-
gap: 4px;
|
|
604
|
-
}
|
|
605
|
-
.wp-cn-exceptions__label {
|
|
606
|
-
font-size: 9px;
|
|
607
|
-
text-transform: uppercase;
|
|
608
|
-
letter-spacing: 0.14em;
|
|
609
|
-
color: var(--wp-text-dim);
|
|
610
|
-
margin-bottom: 2px;
|
|
611
|
-
}
|
|
612
|
-
.wp-cn-exc-row {
|
|
613
|
-
display: flex;
|
|
614
|
-
align-items: center;
|
|
615
|
-
gap: 8px;
|
|
616
|
-
padding: 5px 8px;
|
|
617
|
-
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
618
|
-
border-left: 3px solid transparent;
|
|
619
|
-
background: var(--wp-bg-3);
|
|
620
|
-
}
|
|
621
|
-
.wp-cn-exc-row--boost {
|
|
622
|
-
border-left-color: color-mix(in oklab, var(--wp-success) 60%, transparent);
|
|
623
|
-
background: color-mix(in oklab, var(--wp-success) 6%, var(--wp-bg-3));
|
|
624
|
-
}
|
|
625
|
-
.wp-cn-exc-row--reduce {
|
|
626
|
-
border-left-color: color-mix(in oklab, var(--wp-warn) 60%, transparent);
|
|
627
|
-
background: color-mix(in oklab, var(--wp-warn) 6%, var(--wp-bg-3));
|
|
628
|
-
}
|
|
629
|
-
.wp-cn-exc-row--only {
|
|
630
|
-
border-left-color: color-mix(in oklab, var(--wp-info) 60%, transparent);
|
|
631
|
-
background: color-mix(in oklab, var(--wp-info) 6%, var(--wp-bg-3));
|
|
632
|
-
}
|
|
633
|
-
.wp-cn-exc-row--exclude {
|
|
634
|
-
border-left-color: color-mix(in oklab, var(--wp-danger) 60%, transparent);
|
|
635
|
-
background: color-mix(in oklab, var(--wp-danger) 6%, var(--wp-bg-3));
|
|
636
|
-
}
|
|
637
|
-
.wp-cn-exc__pair {
|
|
638
|
-
flex: 1;
|
|
639
|
-
display: inline-flex;
|
|
640
|
-
align-items: center;
|
|
641
|
-
gap: 6px;
|
|
642
|
-
min-width: 0;
|
|
643
|
-
flex-wrap: wrap;
|
|
644
|
-
}
|
|
645
|
-
.wp-cn-exc__src,
|
|
646
|
-
.wp-cn-exc__tgt {
|
|
647
|
-
padding: 1px 6px;
|
|
648
|
-
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
649
|
-
}
|
|
650
|
-
.wp-cn-exc__src {
|
|
651
|
-
color: var(--wp-constraint-source-text);
|
|
652
|
-
background: color-mix(in oklab, var(--wp-constraint-source) 12%, transparent);
|
|
653
|
-
border: 1px solid color-mix(in oklab, var(--wp-constraint-source) 30%, transparent);
|
|
654
|
-
}
|
|
655
|
-
.wp-cn-exc__tgt {
|
|
656
|
-
color: var(--wp-constraint-target-text);
|
|
657
|
-
background: color-mix(in oklab, var(--wp-constraint-target) 12%, transparent);
|
|
658
|
-
border: 1px solid color-mix(in oklab, var(--wp-constraint-target) 30%, transparent);
|
|
659
|
-
}
|
|
660
|
-
.wp-cn-exc__arrow {
|
|
661
|
-
font-size: 9px;
|
|
662
|
-
color: var(--wp-text-dim);
|
|
663
|
-
}
|
|
664
|
-
.wp-cn-exc__mode {
|
|
665
|
-
display: inline-flex;
|
|
666
|
-
align-items: center;
|
|
667
|
-
gap: 4px;
|
|
668
|
-
font-size: 9px;
|
|
669
|
-
font-weight: 600;
|
|
670
|
-
text-transform: uppercase;
|
|
671
|
-
letter-spacing: 0.04em;
|
|
672
|
-
padding: 2px 7px;
|
|
673
|
-
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
674
|
-
white-space: nowrap;
|
|
675
|
-
border: 1px solid var(--wp-border);
|
|
676
|
-
color: var(--wp-text);
|
|
677
|
-
}
|
|
678
|
-
.wp-cn-exc__glyph {
|
|
679
|
-
font-family: var(--wp-font-mono);
|
|
680
|
-
font-size: 11px;
|
|
681
|
-
line-height: 1;
|
|
682
|
-
}
|
|
683
|
-
.wp-cn-exc__mode--boost {
|
|
684
|
-
color: var(--wp-success);
|
|
685
|
-
background: color-mix(in oklab, var(--wp-success) 20%, transparent);
|
|
686
|
-
border-color: color-mix(in oklab, var(--wp-success) 40%, transparent);
|
|
687
|
-
}
|
|
688
|
-
.wp-cn-exc__mode--reduce {
|
|
689
|
-
color: var(--wp-warn);
|
|
690
|
-
background: color-mix(in oklab, var(--wp-warn) 20%, transparent);
|
|
691
|
-
border-color: color-mix(in oklab, var(--wp-warn) 40%, transparent);
|
|
692
|
-
}
|
|
693
|
-
.wp-cn-exc__mode--only {
|
|
694
|
-
color: var(--wp-info);
|
|
695
|
-
background: color-mix(in oklab, var(--wp-info) 20%, transparent);
|
|
696
|
-
border-color: color-mix(in oklab, var(--wp-info) 40%, transparent);
|
|
697
|
-
}
|
|
698
|
-
.wp-cn-exc__mode--exclude {
|
|
699
|
-
color: var(--wp-danger);
|
|
700
|
-
background: color-mix(in oklab, var(--wp-danger) 20%, transparent);
|
|
701
|
-
border-color: color-mix(in oklab, var(--wp-danger) 40%, transparent);
|
|
702
|
-
}
|
|
703
|
-
.wp-cn-exc__factor {
|
|
704
|
-
font-family: var(--wp-font-mono);
|
|
705
|
-
font-size: 10.5px;
|
|
706
|
-
color: var(--wp-text-muted);
|
|
707
|
-
}
|
|
708
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Read-only grouped-axis constraint matrix preview.
|
|
4
|
+
*
|
|
5
|
+
* Ported from the sister project's interactive `ConstraintMatrix.vue` with all
|
|
6
|
+
* editing interaction stripped: no click/keydown handlers, no CellRulePopover,
|
|
7
|
+
* no Teleport, no sticky-band measurement, no defineExpose, no emit.
|
|
8
|
+
*
|
|
9
|
+
* Two rendering paths:
|
|
10
|
+
* - Grouped: when `targetGroups` or `sourceGroups` are non-empty the grid
|
|
11
|
+
* grows column-axis bands, per-axis row header chips, and a labelled
|
|
12
|
+
* "uncategorized" bucket. The `groupHue`/`groupLabel`/`isBucket` logic
|
|
13
|
+
* comes from `matrix-axis.ts` (Task 2, same repo).
|
|
14
|
+
* - Flat: when both group maps are empty the grid is the community's legacy
|
|
15
|
+
* single-row header layout (back-compat for old payloads that predate axis
|
|
16
|
+
* grouping). Source/target tint applied via CSS vars.
|
|
17
|
+
*
|
|
18
|
+
* Exceptions use `TokenizedValue` so `$var` / `@{ref}` tokens render as
|
|
19
|
+
* coloured chips instead of raw strings. No `v-html`.
|
|
20
|
+
*
|
|
21
|
+
* CSS uses `.wp-cn-*` prefix (community namespace) throughout, with band/chip/
|
|
22
|
+
* eyebrow rules ported from the sister `.wp-mx-*` rules and renamed.
|
|
23
|
+
* Sticky frozen headers are omitted in favour of a simple `overflow-x: auto`
|
|
24
|
+
* wrapper — correctness before complexity for a read-only preview.
|
|
25
|
+
*/
|
|
26
|
+
import { computed } from "vue";
|
|
27
|
+
import TokenizedValue from "./TokenizedValue.vue";
|
|
28
|
+
import {
|
|
29
|
+
groupHue as tagHue,
|
|
30
|
+
groupLabel,
|
|
31
|
+
isBucket,
|
|
32
|
+
orderByGroups,
|
|
33
|
+
toGroups,
|
|
34
|
+
} from "./matrix-axis";
|
|
35
|
+
|
|
36
|
+
const props = defineProps<{
|
|
37
|
+
rows: string[];
|
|
38
|
+
cols: string[];
|
|
39
|
+
matrix: Record<string, Record<string, { mode: string; factor: number }>>;
|
|
40
|
+
sourceName: string;
|
|
41
|
+
targetName: string;
|
|
42
|
+
sourceGroups: Record<string, string[]>;
|
|
43
|
+
targetGroups: Record<string, string[]>;
|
|
44
|
+
exceptions: { source: string; target: string; mode: string; factor: number }[];
|
|
45
|
+
targetSelect: string | null;
|
|
46
|
+
refMap: ReadonlyMap<string, string>;
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
const orderedRows = computed(() => orderByGroups(props.rows, props.sourceGroups));
|
|
50
|
+
const orderedCols = computed(() => orderByGroups(props.cols, props.targetGroups));
|
|
51
|
+
const rowGroups = computed(() => toGroups(orderedRows.value));
|
|
52
|
+
const colGroups = computed(() => toGroups(orderedCols.value));
|
|
53
|
+
const hasColBands = computed(() => colGroups.value.some((g) => !isBucket(g)));
|
|
54
|
+
const hasRowGroups = computed(() => rowGroups.value.some((g) => !isBucket(g)));
|
|
55
|
+
|
|
56
|
+
// `only` (schema v6, linked picks): when the source fires, targets without a
|
|
57
|
+
// rule of their own in that row are excluded.
|
|
58
|
+
type CnMode = "allow" | "boost" | "reduce" | "exclude" | "only";
|
|
59
|
+
const MODE_GLYPH: Record<string, string> = {
|
|
60
|
+
only: "✓",
|
|
61
|
+
boost: "↑",
|
|
62
|
+
reduce: "↓",
|
|
63
|
+
exclude: "×",
|
|
64
|
+
allow: "·",
|
|
65
|
+
};
|
|
66
|
+
const MODE_LABEL: Record<CnMode, string> = {
|
|
67
|
+
boost: "Boost",
|
|
68
|
+
reduce: "Reduce",
|
|
69
|
+
exclude: "Exclude",
|
|
70
|
+
allow: "Neutral",
|
|
71
|
+
only: "Only",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function normalizeMode(raw: string | null | undefined): CnMode {
|
|
75
|
+
return raw === "exclude" || raw === "boost" || raw === "reduce" || raw === "only" ? raw : "allow";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A row holding an `only` cell shuts out its neutral cells at runtime; the
|
|
79
|
+
* preview marks them so the link reads the same as in the extension. */
|
|
80
|
+
function isImpliedOut(row: string, col: string): boolean {
|
|
81
|
+
if (cellAt(row, col).mode !== "allow") return false;
|
|
82
|
+
return Object.values(props.matrix?.[row] ?? {}).some((c) => c?.mode === "only");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cellAt(row: string, col: string): { mode: CnMode; factor: number } {
|
|
86
|
+
const c = props.matrix?.[row]?.[col];
|
|
87
|
+
return {
|
|
88
|
+
mode: normalizeMode(c?.mode),
|
|
89
|
+
factor: typeof c?.factor === "number" ? c.factor : 1,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function fmtFactor(f: number): string {
|
|
94
|
+
if (!Number.isFinite(f)) return "1";
|
|
95
|
+
if (f >= 10) return f.toFixed(0);
|
|
96
|
+
return f.toFixed(2).replace(/\.?0+$/, "");
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<div class="wp-cn">
|
|
102
|
+
<!-- Axis header -->
|
|
103
|
+
<div class="wp-cn-head">
|
|
104
|
+
<span
|
|
105
|
+
class="wp-cn-axis wp-cn-axis--src"
|
|
106
|
+
data-test="cn-axis-src"
|
|
107
|
+
>
|
|
108
|
+
{{ sourceName }}
|
|
109
|
+
</span>
|
|
110
|
+
<i class="pi pi-arrow-right wp-cn-head__arrow" />
|
|
111
|
+
<span
|
|
112
|
+
class="wp-cn-axis wp-cn-axis--tgt"
|
|
113
|
+
data-test="cn-axis-tgt"
|
|
114
|
+
>
|
|
115
|
+
{{ targetName }}
|
|
116
|
+
</span>
|
|
117
|
+
<span
|
|
118
|
+
v-if="targetSelect"
|
|
119
|
+
class="wp-cn-target-select"
|
|
120
|
+
title="Library reach selector — which downstream target instances this constraint covers"
|
|
121
|
+
>
|
|
122
|
+
target: {{ targetSelect }}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Matrix grid -->
|
|
127
|
+
<div
|
|
128
|
+
v-if="rows.length"
|
|
129
|
+
class="wp-cn-grid-wrap"
|
|
130
|
+
>
|
|
131
|
+
<table class="wp-cn-table">
|
|
132
|
+
<thead>
|
|
133
|
+
<!-- Band row: axis names spanning their tags (grouped path only) -->
|
|
134
|
+
<tr>
|
|
135
|
+
<th
|
|
136
|
+
class="wp-cn-corner"
|
|
137
|
+
:rowspan="hasColBands ? 2 : 1"
|
|
138
|
+
>
|
|
139
|
+
<span class="chip">
|
|
140
|
+
<span class="row-1">↓ source</span>
|
|
141
|
+
<span class="row-2">→ target</span>
|
|
142
|
+
</span>
|
|
143
|
+
</th>
|
|
144
|
+
<template v-if="hasColBands">
|
|
145
|
+
<th
|
|
146
|
+
v-for="(grp, gi) in colGroups"
|
|
147
|
+
:key="`band-${gi}`"
|
|
148
|
+
class="wp-cn-band"
|
|
149
|
+
:class="{ 'wp-cn-band--bucket': isBucket(grp) }"
|
|
150
|
+
:colspan="grp.tags.length"
|
|
151
|
+
:style="{ '--ax': tagHue(grp, 'target', true) }"
|
|
152
|
+
:title="groupLabel(grp)"
|
|
153
|
+
>
|
|
154
|
+
<span class="chip">{{ groupLabel(grp) }}</span>
|
|
155
|
+
</th>
|
|
156
|
+
</template>
|
|
157
|
+
<!-- Flat path: single tag header row -->
|
|
158
|
+
<template v-else>
|
|
159
|
+
<th
|
|
160
|
+
v-for="c in orderedCols"
|
|
161
|
+
:key="c.tag"
|
|
162
|
+
class="wp-cn-colhead"
|
|
163
|
+
:style="{ '--ax': 'var(--wp-constraint-target)' }"
|
|
164
|
+
>
|
|
165
|
+
<span class="chip">{{ c.tag }}</span>
|
|
166
|
+
</th>
|
|
167
|
+
</template>
|
|
168
|
+
</tr>
|
|
169
|
+
<!-- Tag row (grouped path only): per-tag headers below the bands -->
|
|
170
|
+
<tr v-if="hasColBands">
|
|
171
|
+
<template
|
|
172
|
+
v-for="(grp, gi) in colGroups"
|
|
173
|
+
:key="`coltags-${gi}`"
|
|
174
|
+
>
|
|
175
|
+
<th
|
|
176
|
+
v-for="(tag, ti) in grp.tags"
|
|
177
|
+
:key="tag"
|
|
178
|
+
class="wp-cn-colhead"
|
|
179
|
+
:class="{ 'group-start': !isBucket(grp) && ti === 0 }"
|
|
180
|
+
:style="{ '--ax': tagHue(grp, 'target', hasColBands) }"
|
|
181
|
+
>
|
|
182
|
+
<span class="chip">{{ tag }}</span>
|
|
183
|
+
</th>
|
|
184
|
+
</template>
|
|
185
|
+
</tr>
|
|
186
|
+
</thead>
|
|
187
|
+
<tbody>
|
|
188
|
+
<template
|
|
189
|
+
v-for="(grp, gi) in rowGroups"
|
|
190
|
+
:key="`rowgrp-${gi}`"
|
|
191
|
+
>
|
|
192
|
+
<!-- Group header chip: named multi-tag axis OR labelled uncategorized bucket -->
|
|
193
|
+
<tr
|
|
194
|
+
v-if="(!isBucket(grp) && grp.tags.length > 1) || (isBucket(grp) && hasRowGroups)"
|
|
195
|
+
class="wp-cn-grp-row"
|
|
196
|
+
>
|
|
197
|
+
<th
|
|
198
|
+
class="wp-cn-grp-head"
|
|
199
|
+
:class="{ 'wp-cn-grp-head--bucket': isBucket(grp) }"
|
|
200
|
+
:colspan="orderedCols.length + 1"
|
|
201
|
+
:style="{ '--ax': tagHue(grp, 'source', true) }"
|
|
202
|
+
:title="groupLabel(grp)"
|
|
203
|
+
>
|
|
204
|
+
<span class="chip">{{ groupLabel(grp) }}</span>
|
|
205
|
+
</th>
|
|
206
|
+
</tr>
|
|
207
|
+
<!-- Tag rows -->
|
|
208
|
+
<tr
|
|
209
|
+
v-for="tag in grp.tags"
|
|
210
|
+
:key="tag"
|
|
211
|
+
>
|
|
212
|
+
<th
|
|
213
|
+
class="wp-cn-rowhead"
|
|
214
|
+
:class="{
|
|
215
|
+
'group-start': !isBucket(grp) && tag === grp.tags[0],
|
|
216
|
+
solo: !isBucket(grp) && grp.tags.length === 1,
|
|
217
|
+
}"
|
|
218
|
+
:style="{ '--ax': tagHue(grp, 'source', hasRowGroups) }"
|
|
219
|
+
>
|
|
220
|
+
<!-- Solo named axis: fold axis name in as an eyebrow above the tag -->
|
|
221
|
+
<span
|
|
222
|
+
v-if="!isBucket(grp) && grp.tags.length === 1"
|
|
223
|
+
class="chip"
|
|
224
|
+
>
|
|
225
|
+
<span class="eye">{{ grp.axisName }}</span>
|
|
226
|
+
<span class="v">{{ tag }}</span>
|
|
227
|
+
</span>
|
|
228
|
+
<span
|
|
229
|
+
v-else
|
|
230
|
+
class="chip"
|
|
231
|
+
>{{ tag }}</span>
|
|
232
|
+
</th>
|
|
233
|
+
<td
|
|
234
|
+
v-for="c in orderedCols"
|
|
235
|
+
:key="c.tag"
|
|
236
|
+
class="wp-cn-td"
|
|
237
|
+
>
|
|
238
|
+
<span
|
|
239
|
+
class="wp-cn-cell"
|
|
240
|
+
:class="{ 'wp-cn-cell--implied-out': isImpliedOut(tag, c.tag) }"
|
|
241
|
+
:data-mode="cellAt(tag, c.tag).mode"
|
|
242
|
+
>
|
|
243
|
+
<span class="wp-cn-cell__glyph">{{ isImpliedOut(tag, c.tag) ? "×" : MODE_GLYPH[cellAt(tag, c.tag).mode] }}</span>
|
|
244
|
+
<span
|
|
245
|
+
v-if="cellAt(tag, c.tag).mode === 'boost' || cellAt(tag, c.tag).mode === 'reduce'"
|
|
246
|
+
class="wp-cn-cell__factor"
|
|
247
|
+
>×{{ fmtFactor(cellAt(tag, c.tag).factor) }}</span>
|
|
248
|
+
</span>
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
</template>
|
|
252
|
+
</tbody>
|
|
253
|
+
</table>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Legend -->
|
|
257
|
+
<div class="wp-cn-legend">
|
|
258
|
+
<span><b data-mode="boost">↑</b> boost</span>
|
|
259
|
+
<span><b data-mode="reduce">↓</b> reduce</span>
|
|
260
|
+
<span><b data-mode="exclude">×</b> exclude</span>
|
|
261
|
+
<span><b data-mode="neutral">·</b> neutral</span>
|
|
262
|
+
<span><b data-mode="only">✓</b> only (links picks)</span>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Exceptions (library-only) -->
|
|
266
|
+
<div
|
|
267
|
+
v-if="exceptions.length"
|
|
268
|
+
class="wp-cn-exceptions"
|
|
269
|
+
>
|
|
270
|
+
<div class="wp-cn-exceptions__label">
|
|
271
|
+
Exceptions
|
|
272
|
+
</div>
|
|
273
|
+
<div
|
|
274
|
+
v-for="(exc, i) in exceptions"
|
|
275
|
+
:key="`exc-${i}`"
|
|
276
|
+
class="wp-cn-exc-row"
|
|
277
|
+
:class="`wp-cn-exc-row--${exc.mode}`"
|
|
278
|
+
:data-test="`cn-exc-${i}`"
|
|
279
|
+
>
|
|
280
|
+
<span class="wp-cn-exc__pair">
|
|
281
|
+
<span class="wp-cn-exc__src">
|
|
282
|
+
<TokenizedValue
|
|
283
|
+
:value="exc.source"
|
|
284
|
+
:uuid-to-name="refMap"
|
|
285
|
+
/>
|
|
286
|
+
</span>
|
|
287
|
+
<i class="pi pi-arrow-right wp-cn-exc__arrow" />
|
|
288
|
+
<span class="wp-cn-exc__tgt">
|
|
289
|
+
<TokenizedValue
|
|
290
|
+
:value="exc.target"
|
|
291
|
+
:uuid-to-name="refMap"
|
|
292
|
+
/>
|
|
293
|
+
</span>
|
|
294
|
+
</span>
|
|
295
|
+
<span
|
|
296
|
+
class="wp-cn-exc__mode"
|
|
297
|
+
:class="`wp-cn-exc__mode--${exc.mode}`"
|
|
298
|
+
>
|
|
299
|
+
<span class="wp-cn-exc__glyph">{{ MODE_GLYPH[exc.mode] }}</span>
|
|
300
|
+
{{ MODE_LABEL[normalizeMode(exc.mode)] }}
|
|
301
|
+
</span>
|
|
302
|
+
<span
|
|
303
|
+
v-if="exc.mode === 'boost' || exc.mode === 'reduce'"
|
|
304
|
+
class="wp-cn-exc__factor"
|
|
305
|
+
>×{{ fmtFactor(exc.factor) }}</span>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</template>
|
|
310
|
+
|
|
311
|
+
<style scoped>
|
|
312
|
+
/* ── Container ─────────────────────────────────────────────── */
|
|
313
|
+
.wp-cn {
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
gap: 14px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ── Axis header ───────────────────────────────────────────── */
|
|
320
|
+
.wp-cn-head {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
gap: 8px;
|
|
324
|
+
flex-wrap: wrap;
|
|
325
|
+
}
|
|
326
|
+
.wp-cn-head__arrow {
|
|
327
|
+
font-size: 11px;
|
|
328
|
+
color: var(--wp-text-dim);
|
|
329
|
+
}
|
|
330
|
+
.wp-cn-axis {
|
|
331
|
+
display: inline-flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
padding: 3px 9px;
|
|
334
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
335
|
+
font-size: 11px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
letter-spacing: 0.02em;
|
|
338
|
+
white-space: nowrap;
|
|
339
|
+
}
|
|
340
|
+
.wp-cn-axis--src {
|
|
341
|
+
color: var(--wp-constraint-source-text);
|
|
342
|
+
background: color-mix(in oklab, var(--wp-constraint-source) 14%, transparent);
|
|
343
|
+
border: 1px solid color-mix(in oklab, var(--wp-constraint-source) 35%, transparent);
|
|
344
|
+
}
|
|
345
|
+
.wp-cn-axis--tgt {
|
|
346
|
+
color: var(--wp-constraint-target-text);
|
|
347
|
+
background: color-mix(in oklab, var(--wp-constraint-target) 14%, transparent);
|
|
348
|
+
border: 1px solid color-mix(in oklab, var(--wp-constraint-target) 35%, transparent);
|
|
349
|
+
}
|
|
350
|
+
.wp-cn-target-select {
|
|
351
|
+
font-size: 10.5px;
|
|
352
|
+
color: var(--wp-text-muted);
|
|
353
|
+
background: var(--wp-bg-3);
|
|
354
|
+
padding: 2px 8px;
|
|
355
|
+
border-radius: var(--wp-radius-cap, 999px);
|
|
356
|
+
font-family: var(--wp-font-mono);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ── Grid wrapper (simple scroll, no sticky headers) ───────── */
|
|
360
|
+
/* Frame hugs the table (align-self instead of stretching to the card width)
|
|
361
|
+
and scrolls on BOTH axes for wide/tall matrices — mirrors the sister's
|
|
362
|
+
inline-block grid. */
|
|
363
|
+
.wp-cn-grid-wrap {
|
|
364
|
+
align-self: flex-start;
|
|
365
|
+
max-width: 100%;
|
|
366
|
+
overflow: auto;
|
|
367
|
+
background: var(--wp-bg-1);
|
|
368
|
+
border: 1px solid var(--wp-border);
|
|
369
|
+
border-radius: min(6px, var(--wp-radius-cap, 999px));
|
|
370
|
+
padding: 0;
|
|
371
|
+
}
|
|
372
|
+
/* `separate` + zero spacing; the 4px inter-cell gap comes from th/td padding,
|
|
373
|
+
so the columns the <th> headers form line up exactly with the body <td>s. */
|
|
374
|
+
.wp-cn-table {
|
|
375
|
+
border-collapse: separate;
|
|
376
|
+
border-spacing: 0;
|
|
377
|
+
}
|
|
378
|
+
.wp-cn-table th,
|
|
379
|
+
.wp-cn-table td {
|
|
380
|
+
vertical-align: middle;
|
|
381
|
+
padding: 2px;
|
|
382
|
+
}
|
|
383
|
+
/* The <td> stays a real table cell (so columns align); the inner
|
|
384
|
+
.wp-cn-cell carries the fixed width + flex layout. Putting flex on the
|
|
385
|
+
<td> itself pulls it out of the table formatting context and breaks
|
|
386
|
+
alignment. */
|
|
387
|
+
.wp-cn-td {
|
|
388
|
+
position: relative;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ── Corner chip ───────────────────────────────────────────── */
|
|
392
|
+
.wp-cn-corner {
|
|
393
|
+
background: var(--wp-bg-2);
|
|
394
|
+
}
|
|
395
|
+
.wp-cn-corner .chip {
|
|
396
|
+
display: flex;
|
|
397
|
+
flex-direction: column;
|
|
398
|
+
justify-content: center;
|
|
399
|
+
width: 96px;
|
|
400
|
+
min-height: 36px;
|
|
401
|
+
height: 100%;
|
|
402
|
+
padding: 5px 8px;
|
|
403
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
404
|
+
font-family: var(--wp-font-mono);
|
|
405
|
+
font-size: 9px;
|
|
406
|
+
line-height: 1.3;
|
|
407
|
+
text-align: left;
|
|
408
|
+
background: color-mix(in srgb, var(--wp-text) 4%, var(--wp-bg-2));
|
|
409
|
+
}
|
|
410
|
+
.wp-cn-corner .row-1 {
|
|
411
|
+
color: var(--wp-constraint-source-text);
|
|
412
|
+
}
|
|
413
|
+
.wp-cn-corner .row-2 {
|
|
414
|
+
color: var(--wp-constraint-target-text);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* ── Column band — axis name spanning its tags ─────────────── */
|
|
418
|
+
.wp-cn-band .chip {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
justify-content: center;
|
|
422
|
+
height: 22px;
|
|
423
|
+
padding: 0 8px;
|
|
424
|
+
font: 700 9px var(--wp-font-sans, sans-serif);
|
|
425
|
+
letter-spacing: 0.06em;
|
|
426
|
+
text-transform: uppercase;
|
|
427
|
+
white-space: nowrap;
|
|
428
|
+
overflow: hidden;
|
|
429
|
+
text-overflow: ellipsis;
|
|
430
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0 0;
|
|
431
|
+
color: color-mix(in srgb, var(--ax) 82%, var(--wp-text));
|
|
432
|
+
background: color-mix(in srgb, var(--ax) 15%, var(--wp-bg-2));
|
|
433
|
+
border-bottom: 2px solid color-mix(in srgb, var(--ax) 55%, transparent);
|
|
434
|
+
}
|
|
435
|
+
/* Uncategorized bucket: dashed edge instead of solid axis accent */
|
|
436
|
+
.wp-cn-band--bucket .chip {
|
|
437
|
+
border-bottom-style: dashed;
|
|
438
|
+
}
|
|
439
|
+
.wp-cn-grp-head--bucket .chip {
|
|
440
|
+
border-left-style: dashed;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ── Column tag chip ───────────────────────────────────────── */
|
|
444
|
+
.wp-cn-colhead .chip {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
justify-content: center;
|
|
448
|
+
width: 64px;
|
|
449
|
+
min-height: 28px;
|
|
450
|
+
padding: 3px 2px;
|
|
451
|
+
font: 700 10px var(--wp-font-sans, sans-serif);
|
|
452
|
+
border-radius: 0 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px));
|
|
453
|
+
color: color-mix(in srgb, var(--ax) 72%, var(--wp-text));
|
|
454
|
+
background: color-mix(in srgb, var(--ax) 9%, var(--wp-bg-2));
|
|
455
|
+
}
|
|
456
|
+
.wp-cn-colhead.group-start .chip {
|
|
457
|
+
border-left: 2px solid color-mix(in srgb, var(--ax) 45%, transparent);
|
|
458
|
+
}
|
|
459
|
+
/* Flat path: col header with the default target tint */
|
|
460
|
+
.wp-cn-colhead {
|
|
461
|
+
color: var(--wp-constraint-target-text);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ── Row group header chip — names the axis above its tags ─── */
|
|
465
|
+
.wp-cn-grp-row .wp-cn-grp-head {
|
|
466
|
+
text-align: left;
|
|
467
|
+
padding: 4px 0 1px;
|
|
468
|
+
}
|
|
469
|
+
.wp-cn-grp-head .chip {
|
|
470
|
+
display: inline-flex;
|
|
471
|
+
align-items: center;
|
|
472
|
+
max-width: 220px;
|
|
473
|
+
height: 22px;
|
|
474
|
+
padding: 0 11px;
|
|
475
|
+
font: 700 9px var(--wp-font-sans, sans-serif);
|
|
476
|
+
letter-spacing: 0.1em;
|
|
477
|
+
text-transform: uppercase;
|
|
478
|
+
white-space: nowrap;
|
|
479
|
+
overflow: hidden;
|
|
480
|
+
text-overflow: ellipsis;
|
|
481
|
+
border-radius: 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0;
|
|
482
|
+
color: color-mix(in srgb, var(--ax) 80%, var(--wp-text));
|
|
483
|
+
background: color-mix(in srgb, var(--ax) 16%, var(--wp-bg-2));
|
|
484
|
+
border-left: 3px solid var(--ax);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* ── Row tag chip ──────────────────────────────────────────── */
|
|
488
|
+
.wp-cn-rowhead .chip {
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
justify-content: flex-start;
|
|
492
|
+
width: 96px;
|
|
493
|
+
height: 36px;
|
|
494
|
+
padding: 0 10px;
|
|
495
|
+
font: 700 10px var(--wp-font-sans, sans-serif);
|
|
496
|
+
text-align: left;
|
|
497
|
+
border-radius: 0 min(4px, var(--wp-radius-cap, 999px)) min(4px, var(--wp-radius-cap, 999px)) 0;
|
|
498
|
+
color: color-mix(in srgb, var(--ax) 76%, var(--wp-text));
|
|
499
|
+
background: color-mix(in srgb, var(--ax) 10%, var(--wp-bg-2));
|
|
500
|
+
border-left: 2px solid color-mix(in srgb, var(--ax) 48%, transparent);
|
|
501
|
+
}
|
|
502
|
+
/* Solo named axis — eyebrow + value stacked */
|
|
503
|
+
.wp-cn-rowhead.solo .chip {
|
|
504
|
+
flex-direction: column;
|
|
505
|
+
align-items: flex-start;
|
|
506
|
+
justify-content: center;
|
|
507
|
+
gap: 2px;
|
|
508
|
+
border-left-width: 3px;
|
|
509
|
+
border-left-color: var(--ax);
|
|
510
|
+
}
|
|
511
|
+
.wp-cn-rowhead.solo .eye {
|
|
512
|
+
max-width: 78px;
|
|
513
|
+
overflow: hidden;
|
|
514
|
+
text-overflow: ellipsis;
|
|
515
|
+
white-space: nowrap;
|
|
516
|
+
font: 700 7px var(--wp-font-sans, sans-serif);
|
|
517
|
+
letter-spacing: 0.08em;
|
|
518
|
+
text-transform: uppercase;
|
|
519
|
+
color: color-mix(in srgb, var(--ax) 72%, var(--wp-text-dim));
|
|
520
|
+
}
|
|
521
|
+
.wp-cn-rowhead.solo .v {
|
|
522
|
+
font: 600 11px var(--wp-font-sans, sans-serif);
|
|
523
|
+
color: var(--wp-text);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* ── Matrix cells ──────────────────────────────────────────── */
|
|
527
|
+
/* Inner cell box — fixed 64×36 so every column lines up; read-only (no
|
|
528
|
+
cursor, no hover lift). */
|
|
529
|
+
.wp-cn-cell {
|
|
530
|
+
width: 64px;
|
|
531
|
+
height: 36px;
|
|
532
|
+
box-sizing: border-box;
|
|
533
|
+
display: inline-flex;
|
|
534
|
+
align-items: center;
|
|
535
|
+
justify-content: center;
|
|
536
|
+
gap: 4px;
|
|
537
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
538
|
+
font-family: var(--wp-font-mono);
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
font-weight: 600;
|
|
541
|
+
}
|
|
542
|
+
.wp-cn-cell__glyph {
|
|
543
|
+
font-size: 13px;
|
|
544
|
+
line-height: 1;
|
|
545
|
+
}
|
|
546
|
+
.wp-cn-cell__factor {
|
|
547
|
+
font-size: 10.5px;
|
|
548
|
+
font-weight: 700;
|
|
549
|
+
}
|
|
550
|
+
.wp-cn-cell[data-mode="neutral"],
|
|
551
|
+
.wp-cn-cell[data-mode="allow"] {
|
|
552
|
+
color: var(--wp-text-dim);
|
|
553
|
+
background: color-mix(in srgb, var(--wp-text) 5%, transparent);
|
|
554
|
+
border: 1px dashed color-mix(in srgb, var(--wp-text) 10%, transparent);
|
|
555
|
+
}
|
|
556
|
+
.wp-cn-cell[data-mode="boost"] {
|
|
557
|
+
color: var(--wp-success);
|
|
558
|
+
background: color-mix(in srgb, var(--wp-success) 22%, transparent);
|
|
559
|
+
border: 1px solid color-mix(in srgb, var(--wp-success) 45%, transparent);
|
|
560
|
+
}
|
|
561
|
+
.wp-cn-cell[data-mode="reduce"] {
|
|
562
|
+
color: var(--wp-warn);
|
|
563
|
+
background: color-mix(in srgb, var(--wp-warn) 22%, transparent);
|
|
564
|
+
border: 1px solid color-mix(in srgb, var(--wp-warn) 45%, transparent);
|
|
565
|
+
}
|
|
566
|
+
.wp-cn-cell[data-mode="only"] {
|
|
567
|
+
color: var(--wp-info);
|
|
568
|
+
background: color-mix(in srgb, var(--wp-info) 22%, transparent);
|
|
569
|
+
border: 1px solid color-mix(in srgb, var(--wp-info) 45%, transparent);
|
|
570
|
+
}
|
|
571
|
+
.wp-cn-cell.wp-cn-cell--implied-out {
|
|
572
|
+
color: color-mix(in srgb, var(--wp-danger) 60%, var(--wp-text-dim));
|
|
573
|
+
border-color: color-mix(in srgb, var(--wp-danger) 30%, transparent);
|
|
574
|
+
}
|
|
575
|
+
.wp-cn-cell[data-mode="exclude"] {
|
|
576
|
+
color: var(--wp-danger);
|
|
577
|
+
background: color-mix(in srgb, var(--wp-danger) 22%, transparent);
|
|
578
|
+
border: 1px solid color-mix(in srgb, var(--wp-danger) 45%, transparent);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* ── Legend ────────────────────────────────────────────────── */
|
|
582
|
+
.wp-cn-legend {
|
|
583
|
+
display: flex;
|
|
584
|
+
flex-wrap: wrap;
|
|
585
|
+
gap: 14px;
|
|
586
|
+
font-size: 11px;
|
|
587
|
+
color: var(--wp-text-muted);
|
|
588
|
+
}
|
|
589
|
+
.wp-cn-legend b {
|
|
590
|
+
font-family: var(--wp-font-mono);
|
|
591
|
+
color: var(--wp-text);
|
|
592
|
+
}
|
|
593
|
+
.wp-cn-legend b[data-mode="boost"] { color: var(--wp-success); }
|
|
594
|
+
.wp-cn-legend b[data-mode="reduce"] { color: var(--wp-warn); }
|
|
595
|
+
.wp-cn-legend b[data-mode="exclude"] { color: var(--wp-danger); }
|
|
596
|
+
.wp-cn-legend b[data-mode="neutral"] { color: var(--wp-text-dim); }
|
|
597
|
+
.wp-cn-legend b[data-mode="only"] { color: var(--wp-info); }
|
|
598
|
+
|
|
599
|
+
/* ── Exceptions ────────────────────────────────────────────── */
|
|
600
|
+
.wp-cn-exceptions {
|
|
601
|
+
display: flex;
|
|
602
|
+
flex-direction: column;
|
|
603
|
+
gap: 4px;
|
|
604
|
+
}
|
|
605
|
+
.wp-cn-exceptions__label {
|
|
606
|
+
font-size: 9px;
|
|
607
|
+
text-transform: uppercase;
|
|
608
|
+
letter-spacing: 0.14em;
|
|
609
|
+
color: var(--wp-text-dim);
|
|
610
|
+
margin-bottom: 2px;
|
|
611
|
+
}
|
|
612
|
+
.wp-cn-exc-row {
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
gap: 8px;
|
|
616
|
+
padding: 5px 8px;
|
|
617
|
+
border-radius: min(4px, var(--wp-radius-cap, 999px));
|
|
618
|
+
border-left: 3px solid transparent;
|
|
619
|
+
background: var(--wp-bg-3);
|
|
620
|
+
}
|
|
621
|
+
.wp-cn-exc-row--boost {
|
|
622
|
+
border-left-color: color-mix(in oklab, var(--wp-success) 60%, transparent);
|
|
623
|
+
background: color-mix(in oklab, var(--wp-success) 6%, var(--wp-bg-3));
|
|
624
|
+
}
|
|
625
|
+
.wp-cn-exc-row--reduce {
|
|
626
|
+
border-left-color: color-mix(in oklab, var(--wp-warn) 60%, transparent);
|
|
627
|
+
background: color-mix(in oklab, var(--wp-warn) 6%, var(--wp-bg-3));
|
|
628
|
+
}
|
|
629
|
+
.wp-cn-exc-row--only {
|
|
630
|
+
border-left-color: color-mix(in oklab, var(--wp-info) 60%, transparent);
|
|
631
|
+
background: color-mix(in oklab, var(--wp-info) 6%, var(--wp-bg-3));
|
|
632
|
+
}
|
|
633
|
+
.wp-cn-exc-row--exclude {
|
|
634
|
+
border-left-color: color-mix(in oklab, var(--wp-danger) 60%, transparent);
|
|
635
|
+
background: color-mix(in oklab, var(--wp-danger) 6%, var(--wp-bg-3));
|
|
636
|
+
}
|
|
637
|
+
.wp-cn-exc__pair {
|
|
638
|
+
flex: 1;
|
|
639
|
+
display: inline-flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
gap: 6px;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
flex-wrap: wrap;
|
|
644
|
+
}
|
|
645
|
+
.wp-cn-exc__src,
|
|
646
|
+
.wp-cn-exc__tgt {
|
|
647
|
+
padding: 1px 6px;
|
|
648
|
+
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
649
|
+
}
|
|
650
|
+
.wp-cn-exc__src {
|
|
651
|
+
color: var(--wp-constraint-source-text);
|
|
652
|
+
background: color-mix(in oklab, var(--wp-constraint-source) 12%, transparent);
|
|
653
|
+
border: 1px solid color-mix(in oklab, var(--wp-constraint-source) 30%, transparent);
|
|
654
|
+
}
|
|
655
|
+
.wp-cn-exc__tgt {
|
|
656
|
+
color: var(--wp-constraint-target-text);
|
|
657
|
+
background: color-mix(in oklab, var(--wp-constraint-target) 12%, transparent);
|
|
658
|
+
border: 1px solid color-mix(in oklab, var(--wp-constraint-target) 30%, transparent);
|
|
659
|
+
}
|
|
660
|
+
.wp-cn-exc__arrow {
|
|
661
|
+
font-size: 9px;
|
|
662
|
+
color: var(--wp-text-dim);
|
|
663
|
+
}
|
|
664
|
+
.wp-cn-exc__mode {
|
|
665
|
+
display: inline-flex;
|
|
666
|
+
align-items: center;
|
|
667
|
+
gap: 4px;
|
|
668
|
+
font-size: 9px;
|
|
669
|
+
font-weight: 600;
|
|
670
|
+
text-transform: uppercase;
|
|
671
|
+
letter-spacing: 0.04em;
|
|
672
|
+
padding: 2px 7px;
|
|
673
|
+
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
674
|
+
white-space: nowrap;
|
|
675
|
+
border: 1px solid var(--wp-border);
|
|
676
|
+
color: var(--wp-text);
|
|
677
|
+
}
|
|
678
|
+
.wp-cn-exc__glyph {
|
|
679
|
+
font-family: var(--wp-font-mono);
|
|
680
|
+
font-size: 11px;
|
|
681
|
+
line-height: 1;
|
|
682
|
+
}
|
|
683
|
+
.wp-cn-exc__mode--boost {
|
|
684
|
+
color: var(--wp-success);
|
|
685
|
+
background: color-mix(in oklab, var(--wp-success) 20%, transparent);
|
|
686
|
+
border-color: color-mix(in oklab, var(--wp-success) 40%, transparent);
|
|
687
|
+
}
|
|
688
|
+
.wp-cn-exc__mode--reduce {
|
|
689
|
+
color: var(--wp-warn);
|
|
690
|
+
background: color-mix(in oklab, var(--wp-warn) 20%, transparent);
|
|
691
|
+
border-color: color-mix(in oklab, var(--wp-warn) 40%, transparent);
|
|
692
|
+
}
|
|
693
|
+
.wp-cn-exc__mode--only {
|
|
694
|
+
color: var(--wp-info);
|
|
695
|
+
background: color-mix(in oklab, var(--wp-info) 20%, transparent);
|
|
696
|
+
border-color: color-mix(in oklab, var(--wp-info) 40%, transparent);
|
|
697
|
+
}
|
|
698
|
+
.wp-cn-exc__mode--exclude {
|
|
699
|
+
color: var(--wp-danger);
|
|
700
|
+
background: color-mix(in oklab, var(--wp-danger) 20%, transparent);
|
|
701
|
+
border-color: color-mix(in oklab, var(--wp-danger) 40%, transparent);
|
|
702
|
+
}
|
|
703
|
+
.wp-cn-exc__factor {
|
|
704
|
+
font-family: var(--wp-font-mono);
|
|
705
|
+
font-size: 10.5px;
|
|
706
|
+
color: var(--wp-text-muted);
|
|
707
|
+
}
|
|
708
|
+
</style>
|