@dumiflex/wp-kit 0.5.0 → 0.7.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +5 -0
  3. package/package.json +4 -3
  4. package/src/Avatar.vue +1 -1
  5. package/src/Banner.vue +4 -4
  6. package/src/BundlePreview.vue +254 -0
  7. package/src/Button.vue +73 -44
  8. package/src/Checkbox.vue +201 -63
  9. package/src/Chip.vue +5 -2
  10. package/src/CodeBlock.vue +1 -1
  11. package/src/ColorField.vue +60 -6
  12. package/src/CombinePreview.vue +150 -0
  13. package/src/Combobox.vue +10 -14
  14. package/src/ConfirmModal.vue +12 -22
  15. package/src/ConstraintMatrixPreview.vue +6 -22
  16. package/src/ConstraintPreview.vue +259 -0
  17. package/src/ContextMenu.vue +11 -30
  18. package/src/CopyButton.vue +1 -1
  19. package/src/DataTable.vue +0 -2
  20. package/src/DatePicker.vue +1 -1
  21. package/src/DerivationCondition.vue +68 -0
  22. package/src/DerivationPreview.vue +345 -0
  23. package/src/Disclosure.vue +2 -1
  24. package/src/Drawer.vue +2 -2
  25. package/src/DropZone.vue +2 -2
  26. package/src/FixedValuesPreview.vue +184 -0
  27. package/src/HsvPicker.vue +2 -2
  28. package/src/IconPicker.vue +32 -103
  29. package/src/IconPickerPanel.vue +132 -0
  30. package/src/ImageGrid.vue +2 -2
  31. package/src/Lightbox.vue +2 -2
  32. package/src/LoadingMark.vue +6 -61
  33. package/src/Modal.vue +109 -44
  34. package/src/ModalShell.vue +26 -5
  35. package/src/Pagination.vue +4 -4
  36. package/src/PayloadPreview.vue +154 -0
  37. package/src/Popover.vue +68 -12
  38. package/src/PostDetailSkeleton.vue +65 -0
  39. package/src/PostGridSkeleton.vue +61 -0
  40. package/src/PreviewGroup.vue +75 -0
  41. package/src/ProfileSkeleton.vue +73 -0
  42. package/src/RadioGroup.vue +5 -4
  43. package/src/RichText.vue +61 -0
  44. package/src/SearchInput.vue +1 -1
  45. package/src/Select.vue +53 -158
  46. package/src/SelectionBar.vue +90 -0
  47. package/src/SettingGroup.vue +57 -0
  48. package/src/SettingRow.vue +127 -0
  49. package/src/Skeleton.vue +1 -1
  50. package/src/Slider.vue +166 -55
  51. package/src/SortableList.vue +1 -1
  52. package/src/TagInput.vue +1 -1
  53. package/src/Timeline.vue +2 -2
  54. package/src/WildcardPreview.vue +607 -0
  55. package/src/WorkingLine.vue +234 -0
  56. package/src/WpIconButton.vue +3 -2
  57. package/src/WpImageZoom.vue +7 -7
  58. package/src/WpSegmented.vue +3 -1
  59. package/src/WpSwitch.vue +1 -1
  60. package/src/WpTabs.vue +1 -1
  61. package/src/_modal-motion.css +1 -1
  62. package/src/button.css +8 -6
  63. package/src/canvas-motion.ts +251 -0
  64. package/src/floating.ts +5 -0
  65. package/src/hsv.ts +1 -1
  66. package/src/kind-colour.ts +24 -1
  67. package/src/linkify.ts +142 -0
  68. package/src/loading-mark-motion.ts +12 -155
  69. package/src/matrix-axis.ts +28 -0
  70. package/src/menu.css +51 -0
  71. package/src/motion.css +73 -0
  72. package/src/pi-icons.ts +51 -0
  73. package/src/preview-readers.ts +401 -0
  74. package/src/preview.css +194 -0
  75. package/src/slider-math.ts +45 -1
  76. package/src/theme/looks.css +6 -19
  77. package/src/theme/tokens.css +12 -1108
  78. package/src/tokens.ts +15 -4
  79. package/src/tooltip.css +8 -5
  80. package/src/useDialogFocus.ts +11 -2
  81. package/src/working-line-motion.ts +135 -0
  82. package/src/CloseButton.vue +0 -69
  83. package/src/ColorPicker.vue +0 -201
  84. package/src/ConfirmDialog.vue +0 -165
  85. package/src/RangeSlider.vue +0 -183
  86. package/src/Toggle.vue +0 -36
  87. package/src/WpButton.vue +0 -75
  88. package/src/WpCheck.vue +0 -104
  89. package/src/_skeleton.css +0 -50
package/src/Combobox.vue CHANGED
@@ -175,7 +175,7 @@ function setOpen(v: boolean): void {
175
175
  <!-- Popover's autofocus prop is off here: focus stays in the box you type in. -->
176
176
  <!-- eslint-disable vuejs-accessibility/no-autofocus -->
177
177
  <template>
178
- <Popover :open="open" role="group" match-width :autofocus="false" :label="`${ariaLabel} choices`" @update:open="setOpen">
178
+ <Popover :open="open" variant="menu" role="group" match-width :max-height="280" :autofocus="false" :label="`${ariaLabel} choices`" @update:open="setOpen">
179
179
  <template #trigger>
180
180
  <!-- A click on the box's padding focuses the text field, which has the keys. -->
181
181
  <!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/no-static-element-interactions -->
@@ -245,7 +245,7 @@ function setOpen(v: boolean): void {
245
245
  v-for="(o, i) in matches"
246
246
  :id="`${listId}-${i}`"
247
247
  :key="String(o.value)"
248
- class="wp-combo__opt"
248
+ class="wp-combo__opt wp-menu__item"
249
249
  role="option"
250
250
  :aria-selected="isPicked(o.value)"
251
251
  :aria-disabled="o.disabled || undefined"
@@ -265,6 +265,7 @@ function setOpen(v: boolean): void {
265
265
  </template>
266
266
 
267
267
  <style scoped src="./field.css"></style>
268
+ <style scoped src="./menu.css"></style>
268
269
 
269
270
  <style scoped>
270
271
  .wp-combo {
@@ -284,7 +285,7 @@ function setOpen(v: boolean): void {
284
285
  color: var(--wp-text); font-size: var(--wp-text-xs); line-height: 1;
285
286
  }
286
287
  .wp-combo[data-size="sm"] .wp-combo__chip { height: 16px; padding-left: 5px; font-size: 10.5px; }
287
- .wp-combo__chip-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
288
+ .wp-combo__chip-text { min-width: 0; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
288
289
  .wp-combo__remove, .wp-combo__clear {
289
290
  display: inline-grid; place-items: center; width: 16px; height: 16px; padding: 0;
290
291
  border: 0; border-radius: min(50%, var(--wp-radius-cap, 999px)); background: transparent; color: var(--wp-text-muted); cursor: pointer;
@@ -299,7 +300,7 @@ function setOpen(v: boolean): void {
299
300
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
300
301
  color: var(--wp-text-muted); font-size: 10px; pointer-events: none;
301
302
  }
302
- .wp-combo[data-size="sm"] .wp-combo__caret { right: 8px; font-size: 9px; }
303
+ .wp-combo[data-size="sm"] .wp-combo__caret { right: 8px; font-size: 10px; }
303
304
  .wp-combo__field {
304
305
  flex: 1 0 6ch; min-width: 6ch; height: calc(var(--_h) - 8px); margin: 0; padding: 0 2px;
305
306
  border: 0; outline: none; background: transparent; color: inherit; font: inherit;
@@ -311,20 +312,15 @@ function setOpen(v: boolean): void {
311
312
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
312
313
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
313
314
  }
314
- .wp-combo__list { margin: calc(var(--wp-space-2) * -1); padding: 0; list-style: none; max-height: 280px; overflow: auto; }
315
- .wp-combo__opt {
316
- display: flex; align-items: flex-start; gap: var(--wp-space-2);
317
- padding: 6px var(--wp-space-4) 6px var(--wp-space-2); border-radius: var(--wp-radius-sm);
318
- color: var(--wp-text); font-size: var(--wp-text-sm); cursor: pointer;
319
- }
320
- .wp-combo__opt:hover, .wp-combo__opt[data-active] { background: var(--wp-row-hover); }
321
- .wp-combo__opt[data-active] { box-shadow: inset 2px 0 0 var(--wp-accent); }
322
- .wp-combo__opt[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
315
+ /* The panel and the rows' padding, corners, text and states are the shared
316
+ * menu look (Popover variant="menu" + menu.css); the list scrolls in the panel. */
317
+ .wp-combo__list { margin: 0; padding: 0; list-style: none; }
318
+ .wp-combo__opt { display: flex; align-items: flex-start; gap: var(--wp-space-2); }
323
319
  .wp-combo__tick { width: 14px; margin-top: 2px; font-size: 10px; color: var(--wp-accent); visibility: hidden; }
324
320
  .wp-combo__tick[data-on] { visibility: visible; }
325
321
  .wp-combo__opt-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
326
322
  .wp-combo__hint { color: var(--wp-text-muted); font-size: var(--wp-text-xs); }
327
- .wp-combo__empty { padding: 6px var(--wp-space-4); color: var(--wp-text-muted); font-size: var(--wp-text-sm); }
323
+ .wp-combo__empty { padding: 6px 8px; color: var(--wp-text-muted); font-size: 12px; }
328
324
  @media (prefers-reduced-motion: reduce) {
329
325
  .wp-combo { transition: none; }
330
326
  }
@@ -2,22 +2,23 @@
2
2
  /**
3
3
  * Ask before an action, in the manager's own dialog.
4
4
  *
5
- * A thin wrapper on Modal: the same backdrop, focus trap, Escape and focus
6
- * return as every other manager dialog, with a Cancel button and a confirm
7
- * button that names the action. The canvas keeps `ConfirmDialog`, which
8
- * wears the node colours; this one wears the manager's.
5
+ * Built on Modal: the same backdrop, focus trap, Escape and focus return as
6
+ * every other dialog, with a Cancel button and a confirm button that names
7
+ * the action. One confirm for the manager, the canvas and the community site
8
+ * (ConfirmDialog was folded in: `visible` → `v-model:open`/`open`,
9
+ * `variant="danger"` → `tone="danger"`); it wears the colours of its side.
9
10
  *
10
11
  * Escape, the backdrop, the ✕ and Cancel all emit `cancel`. Enter confirms
11
12
  * unless focus is on a button or a text box (which then gets the key itself).
12
- * The `confirm-*` data-test names match ConfirmDialog's, so a test can drive
13
- * either.
13
+ * Data-test names: `confirm-title`, `confirm-body`, `confirm-cancel`,
14
+ * `confirm-confirm`.
14
15
  *
15
16
  * `requireType` makes the user type that exact text before Confirm works
16
17
  * (deleting an account); `confirmDisabled` holds Confirm off for a gate the
17
18
  * page owns (a required reason in the slot). `warn` asks about something
18
19
  * that can be undone but matters: an outlined button and an info mark.
19
20
  */
20
- import { computed, onBeforeUnmount, ref, watch } from "vue";
21
+ import { computed, ref, watch } from "vue";
21
22
  import Modal from "./Modal.vue";
22
23
  import Button from "./Button.vue";
23
24
  import Icon from "./Icon.vue";
@@ -70,26 +71,15 @@ function onKeydown(ev: KeyboardEvent): void {
70
71
  emit("confirm");
71
72
  }
72
73
 
73
- watch(
74
- () => props.open,
75
- (open) => {
76
- if (open) {
77
- typed.value = "";
78
- window.addEventListener("keydown", onKeydown);
79
- }
80
- else window.removeEventListener("keydown", onKeydown);
81
- },
82
- { immediate: true },
83
- );
84
- onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
74
+ watch(() => props.open, (open) => { if (open) typed.value = ""; }, { immediate: true });
85
75
  </script>
86
76
 
87
77
  <template>
88
- <Modal :open="open" size="sm" @update:open="onOpen">
78
+ <Modal :open="open" size="sm" @update:open="onOpen" @keydown="onKeydown">
89
79
  <template #header="{ titleId }">
90
80
  <span class="wp-confirm-modal__head" :class="`wp-confirm-modal__head--${tone}`">
91
81
  <Icon :name="tone === 'danger' ? 'exclamation-triangle' : 'info-circle'" />
92
- <h3 :id="titleId" class="wp-modal__title" data-test="confirm-title">{{ title }}</h3>
82
+ <h3 :id="titleId" class="wp-confirm-modal__title" data-test="confirm-title">{{ title }}</h3>
93
83
  </span>
94
84
  </template>
95
85
  <div class="wp-confirm-modal__body">
@@ -123,7 +113,7 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown));
123
113
  }
124
114
  .wp-confirm-modal__head--danger { color: var(--wp-danger); }
125
115
  .wp-confirm-modal__head--warn { color: var(--wp-warn); }
126
- .wp-confirm-modal__head .wp-modal__title { color: var(--wp-text); }
116
+ .wp-confirm-modal__title { margin: 0; font-size: var(--wp-text-md); font-weight: 600; color: var(--wp-text); }
127
117
  .wp-confirm-modal__text {
128
118
  margin: 0;
129
119
  color: var(--wp-text-muted);
@@ -29,8 +29,12 @@ import {
29
29
  groupHue as tagHue,
30
30
  groupLabel,
31
31
  isBucket,
32
+ MODE_GLYPH,
33
+ MODE_LABEL,
34
+ normalizeMode,
32
35
  orderByGroups,
33
36
  toGroups,
37
+ type CellMode,
34
38
  } from "./matrix-axis";
35
39
 
36
40
  const props = defineProps<{
@@ -53,27 +57,7 @@ const colGroups = computed(() => toGroups(orderedCols.value));
53
57
  const hasColBands = computed(() => colGroups.value.some((g) => !isBucket(g)));
54
58
  const hasRowGroups = computed(() => rowGroups.value.some((g) => !isBucket(g)));
55
59
 
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
- }
60
+ type CnMode = CellMode;
77
61
 
78
62
  /** A row holding an `only` cell shuts out its neutral cells at runtime; the
79
63
  * preview marks them so the link reads the same as in the extension. */
@@ -296,7 +280,7 @@ function fmtFactor(f: number): string {
296
280
  class="wp-cn-exc__mode"
297
281
  :class="`wp-cn-exc__mode--${exc.mode}`"
298
282
  >
299
- <span class="wp-cn-exc__glyph">{{ MODE_GLYPH[exc.mode] }}</span>
283
+ <span class="wp-cn-exc__glyph">{{ MODE_GLYPH[normalizeMode(exc.mode)] }}</span>
300
284
  {{ MODE_LABEL[normalizeMode(exc.mode)] }}
301
285
  </span>
302
286
  <span
@@ -0,0 +1,259 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Constraint: counts up top, then the grouped matrix (ConstraintMatrixPreview)
4
+ * or, for a big matrix, a searchable list of only the cells that do
5
+ * something. The search narrows rows / columns in both views.
6
+ */
7
+ import { computed, ref } from "vue";
8
+ import ConstraintMatrixPreview from "./ConstraintMatrixPreview.vue";
9
+ import { MODE_GLYPH as GLYPH, MODE_LABEL as LABEL, normalizeMode, type CellMode as Mode } from "./matrix-axis";
10
+ import {
11
+ readArray,
12
+ readGroups,
13
+ readNumber,
14
+ readObject,
15
+ readString,
16
+ type Obj,
17
+ } from "./preview-readers";
18
+ import WpSegmented, { type SegmentedOption } from "./WpSegmented.vue";
19
+ import SearchInput from "./SearchInput.vue";
20
+
21
+ const props = defineProps<{
22
+ inner: Obj | null;
23
+ refMap: ReadonlyMap<string, string>;
24
+ refNames: Record<string, string>;
25
+ refGroups: Record<string, Record<string, string[]>>;
26
+ }>();
27
+
28
+ interface Cell {
29
+ mode: Mode;
30
+ factor: number;
31
+ }
32
+
33
+ const matrix = computed<Record<string, Record<string, Cell>>>(() => {
34
+ const m = readObject(props.inner, "matrix");
35
+ if (!m) return {};
36
+ const out: Record<string, Record<string, Cell>> = {};
37
+ for (const row of Object.keys(m)) {
38
+ const byCol = readObject(m, row);
39
+ if (!byCol) continue;
40
+ out[row] = {};
41
+ for (const col of Object.keys(byCol)) {
42
+ const e = readObject(byCol, col);
43
+ if (!e) continue;
44
+ out[row][col] = { mode: normalizeMode(readString(e, "mode")), factor: readNumber(e, "factor") ?? 1 };
45
+ }
46
+ }
47
+ return out;
48
+ });
49
+ const rows = computed(() => Object.keys(matrix.value));
50
+ /** Union of every row's columns, first-seen order. */
51
+ const cols = computed(() => {
52
+ const seen = new Set<string>();
53
+ for (const r of rows.value) for (const c of Object.keys(matrix.value[r] ?? {})) seen.add(c);
54
+ return [...seen];
55
+ });
56
+ /** Axis names: cached name → dependency name → raw id → role word. */
57
+ function axisName(idKey: string, nameKey: string, role: string): string {
58
+ const id = readString(props.inner, idKey);
59
+ return readString(props.inner, nameKey) ?? (id ? props.refNames[id] : undefined) ?? id ?? role;
60
+ }
61
+ const sourceName = computed(() => axisName("source_wildcard_id", "source_wildcard_name", "source"));
62
+ const targetName = computed(() => axisName("target_wildcard_id", "target_wildcard_name", "target"));
63
+ /** Embedded axis snapshot first, then the dependency's tag_groups. */
64
+ function axisGroups(embeddedKey: string, idKey: string): Record<string, string[]> {
65
+ const embedded = readGroups(props.inner, embeddedKey);
66
+ if (embedded) return embedded;
67
+ const id = readString(props.inner, idKey);
68
+ return (id ? props.refGroups[id] : undefined) ?? {};
69
+ }
70
+ const sourceGroups = computed(() => axisGroups("source_groups", "source_wildcard_id"));
71
+ const targetGroups = computed(() => axisGroups("target_groups", "target_wildcard_id"));
72
+ /** Library exceptions only; instance `extra_exceptions` never publish. */
73
+ const exceptions = computed(() =>
74
+ readArray(props.inner, "exceptions").map((e) => ({
75
+ source: readString(e, "source_value") ?? readString(e, "source") ?? "",
76
+ target: readString(e, "target_value") ?? readString(e, "target") ?? "",
77
+ mode: normalizeMode(readString(e, "mode")),
78
+ factor: readNumber(e, "factor") ?? 1,
79
+ })),
80
+ );
81
+ const targetSelect = computed(() => {
82
+ const sel = readObject(props.inner, "target_select");
83
+ const mode = readString(sel, "mode");
84
+ if (!mode) return null;
85
+ const count = readNumber(sel, "count");
86
+ return count !== null ? `${mode} ${count}` : mode;
87
+ });
88
+
89
+ const ruleCells = computed(() => {
90
+ const out: { row: string; col: string; cell: Cell }[] = [];
91
+ for (const r of rows.value)
92
+ for (const c of cols.value) {
93
+ const cell = matrix.value[r]?.[c];
94
+ if (cell && cell.mode !== "allow") out.push({ row: r, col: c, cell });
95
+ }
96
+ return out;
97
+ });
98
+ const excludeCount = computed(() => ruleCells.value.filter((x) => x.cell.mode === "exclude").length);
99
+
100
+ // ─── search + view ───
101
+ const search = ref("");
102
+ const q = computed(() => search.value.trim().toLowerCase());
103
+ const shownRows = computed(() => {
104
+ if (!q.value) return rows.value;
105
+ const hit = rows.value.filter((r) => r.toLowerCase().includes(q.value));
106
+ return hit.length ? hit : rows.value;
107
+ });
108
+ const shownCols = computed(() => {
109
+ if (!q.value) return cols.value;
110
+ const hit = cols.value.filter((c) => c.toLowerCase().includes(q.value));
111
+ return hit.length ? hit : cols.value;
112
+ });
113
+ const big = computed(() => rows.value.length * cols.value.length > 60);
114
+ const view = ref<"matrix" | "list">("matrix");
115
+ const VIEW_OPTIONS: SegmentedOption<"matrix" | "list">[] = [
116
+ { value: "matrix", label: "Matrix" },
117
+ { value: "list", label: "Rules list" },
118
+ ];
119
+ const listRows = computed(() =>
120
+ ruleCells.value.filter(
121
+ (x) => !q.value || x.row.toLowerCase().includes(q.value) || x.col.toLowerCase().includes(q.value),
122
+ ),
123
+ );
124
+ function fmtFactor(f: number): string {
125
+ return Number.isInteger(f) ? String(f) : f.toFixed(2).replace(/\.?0+$/, "");
126
+ }
127
+ </script>
128
+
129
+ <template>
130
+ <div class="wp-pv">
131
+ <div class="wp-pv-stats">
132
+ <div class="wp-pv-stat">
133
+ <b>{{ rows.length }} × {{ cols.length }}</b><span>sources × targets</span>
134
+ </div>
135
+ <div class="wp-pv-stat">
136
+ <b>{{ ruleCells.length }}</b><span>rules set</span>
137
+ </div>
138
+ <div
139
+ v-if="excludeCount"
140
+ class="wp-pv-stat"
141
+ >
142
+ <b>{{ excludeCount }}</b><span>excludes</span>
143
+ </div>
144
+ <div
145
+ v-if="exceptions.length"
146
+ class="wp-pv-stat"
147
+ >
148
+ <b>{{ exceptions.length }}</b><span>exception{{ exceptions.length === 1 ? "" : "s" }}</span>
149
+ </div>
150
+ <div
151
+ v-if="targetSelect"
152
+ class="wp-pv-stat"
153
+ >
154
+ <b>{{ targetSelect }}</b><span>reach</span>
155
+ </div>
156
+ </div>
157
+ <div
158
+ v-if="big"
159
+ class="wp-pv-tool"
160
+ >
161
+ <SearchInput
162
+ v-model="search"
163
+ placeholder="Filter rows or columns…"
164
+ data-test="constraint-search"
165
+ class="wp-pv-search"
166
+ />
167
+ <span class="wp-pv-label">Show</span>
168
+ <WpSegmented
169
+ v-model="view"
170
+ :options="VIEW_OPTIONS"
171
+ size="sm"
172
+ aria-label="Show"
173
+ test-prefix="constraint"
174
+ />
175
+ </div>
176
+ <div
177
+ v-if="view === 'matrix'"
178
+ class="wp-cn-wrap"
179
+ >
180
+ <ConstraintMatrixPreview
181
+ :rows="shownRows"
182
+ :cols="shownCols"
183
+ :matrix="matrix"
184
+ :source-name="sourceName"
185
+ :target-name="targetName"
186
+ :source-groups="sourceGroups"
187
+ :target-groups="targetGroups"
188
+ :exceptions="exceptions"
189
+ :target-select="targetSelect"
190
+ :ref-map="refMap"
191
+ />
192
+ </div>
193
+ <template v-else>
194
+ <table class="wp-pv-table">
195
+ <thead>
196
+ <tr>
197
+ <th>{{ sourceName }}</th>
198
+ <th>{{ targetName }}</th>
199
+ <th>Rule</th>
200
+ </tr>
201
+ </thead>
202
+ <tbody>
203
+ <tr
204
+ v-for="x in listRows"
205
+ :key="`${x.row}|${x.col}`"
206
+ data-test="constraint-rule"
207
+ >
208
+ <td>{{ x.row }}</td>
209
+ <td>{{ x.col }}</td>
210
+ <td>
211
+ <span
212
+ class="wp-cn-rule"
213
+ :data-mode="x.cell.mode"
214
+ >{{ GLYPH[x.cell.mode] }} {{ LABEL[x.cell.mode] }}<template v-if="x.cell.mode === 'boost' || x.cell.mode === 'reduce'"> ×{{ fmtFactor(x.cell.factor) }}</template></span>
215
+ </td>
216
+ </tr>
217
+ </tbody>
218
+ </table>
219
+ <p
220
+ v-if="!listRows.length"
221
+ class="wp-pv-empty"
222
+ >
223
+ No rules match.
224
+ </p>
225
+ </template>
226
+ </div>
227
+ </template>
228
+
229
+ <style scoped src="./preview.css"></style>
230
+ <style scoped>
231
+ .wp-cn-wrap {
232
+ padding: 14px 16px 16px;
233
+ min-width: 0;
234
+ }
235
+ .wp-cn-rule {
236
+ font-family: var(--wp-font-mono);
237
+ font-size: 11.5px;
238
+ font-weight: 600;
239
+ padding: 2px 7px;
240
+ border-radius: min(5px, var(--wp-radius-cap, 999px));
241
+ background: var(--wp-bg-3);
242
+ }
243
+ .wp-cn-rule[data-mode="boost"] {
244
+ color: var(--wp-success);
245
+ background: color-mix(in oklab, var(--wp-success) 14%, transparent);
246
+ }
247
+ .wp-cn-rule[data-mode="reduce"] {
248
+ color: var(--wp-warn);
249
+ background: color-mix(in oklab, var(--wp-warn) 14%, transparent);
250
+ }
251
+ .wp-cn-rule[data-mode="exclude"] {
252
+ color: var(--wp-danger-text, var(--wp-danger));
253
+ background: color-mix(in oklab, var(--wp-danger) 15%, transparent);
254
+ }
255
+ .wp-cn-rule[data-mode="only"] {
256
+ color: color-mix(in oklab, var(--wp-info) 55%, white);
257
+ background: color-mix(in oklab, var(--wp-info) 16%, transparent);
258
+ }
259
+ </style>
@@ -204,7 +204,7 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
204
204
  <ul
205
205
  v-if="visible"
206
206
  ref="menuEl"
207
- class="wp-ctxmenu"
207
+ class="wp-ctxmenu wp-menu"
208
208
  :style="{ left: `${pos.x}px`, top: `${pos.y}px` }"
209
209
  role="menu"
210
210
  @click.stop
@@ -235,7 +235,8 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
235
235
  can't see it); keys come from the menu's own keydown. -->
236
236
  <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions, vuejs-accessibility/click-events-have-key-events, vuejs-accessibility/mouse-events-have-key-events -->
237
237
  <li
238
- class="wp-ctxmenu__item"
238
+ class="wp-ctxmenu__item wp-menu__item"
239
+ :data-active="activeIndex === i || undefined"
239
240
  :class="{
240
241
  'wp-ctxmenu__item--danger': entry.danger,
241
242
  'wp-ctxmenu__item--accent': entry.accent,
@@ -268,26 +269,16 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
268
269
  </Teleport>
269
270
  </template>
270
271
 
272
+ <style scoped src="./menu.css"></style>
273
+
271
274
  <style scoped>
275
+ /* The panel (background, border, corners, padding, shadow, layer, text) and
276
+ * the rows' padding, corners and hover / active / disabled states are the
277
+ * shared menu look (menu.css), the same as Select's and Combobox's lists. */
272
278
  .wp-ctxmenu {
273
279
  position: fixed;
274
- z-index: 10000;
275
- background: var(--wp-bg-2);
276
- border: 1px solid var(--wp-border-strong);
277
- border-radius: var(--wp-radius-sm);
278
- box-shadow: var(--wp-shadow-md);
279
- /* Tighter padding than the old 4px-0 to match the v2 mockup. Items
280
- * carry their own 6px padding + rounded corners so the menu reads
281
- * as a stack of pills, not a flat list. */
282
- padding: 4px;
283
- /* Bumped from 180px to 250px — subtitles wrap awkwardly at narrower
284
- * widths. Existing callers without subtitles still fit comfortably. */
280
+ /* Subtitles wrap awkwardly below 250px. */
285
281
  min-width: 250px;
286
- list-style: none;
287
- margin: 0;
288
- font-family: var(--wp-font-sans, sans-serif);
289
- font-size: 12px;
290
- color: var(--wp-text);
291
282
  user-select: none;
292
283
  }
293
284
  /* Header — scope label at the top of the menu. Border-bottom acts as
@@ -319,21 +310,16 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
319
310
  padding: 6px 8px 4px 8px;
320
311
  }
321
312
  .wp-ctxmenu__item {
322
- padding: 6px 8px;
323
- border-radius: min(4px, var(--wp-radius-cap, 999px));
324
- cursor: pointer;
325
313
  white-space: nowrap;
326
314
  display: flex;
327
315
  align-items: center;
328
316
  gap: 8px;
329
317
  }
330
- .wp-ctxmenu__item--active,
331
- .wp-ctxmenu__item:hover { background: var(--wp-accent-glow); color: var(--wp-accent); }
332
318
  /* A checkable item's tick: always takes its room, shows only when on. */
333
319
  .wp-ctxmenu__check { width: 14px; font-size: 11px; text-align: center; visibility: hidden; color: var(--wp-accent); }
334
320
  .wp-ctxmenu__check[data-on] { visibility: visible; }
335
321
  .wp-ctxmenu__item--danger { color: var(--wp-red); }
336
- .wp-ctxmenu__item--danger.wp-ctxmenu__item--active,
322
+ .wp-ctxmenu__item--danger[data-active="true"],
337
323
  .wp-ctxmenu__item--danger:hover { background: var(--wp-red-bg); color: var(--wp-red); }
338
324
  /* Accent variant — recommended action highlight. Tinted background +
339
325
  * accent text in idle state so the eye lands on it; hover uses the
@@ -342,16 +328,11 @@ function isSectionEntry(entry: ContextMenuEntry): entry is ContextMenuSection {
342
328
  color: var(--wp-accent);
343
329
  background: color-mix(in oklab, var(--wp-accent) 12%, transparent);
344
330
  }
345
- .wp-ctxmenu__item--accent.wp-ctxmenu__item--active,
331
+ .wp-ctxmenu__item--accent[data-active="true"],
346
332
  .wp-ctxmenu__item--accent:hover {
347
333
  background: var(--wp-accent-glow);
348
334
  color: var(--wp-accent);
349
335
  }
350
- .wp-ctxmenu__item--disabled {
351
- color: var(--wp-text-dim);
352
- cursor: not-allowed;
353
- }
354
- .wp-ctxmenu__item--disabled:hover { background: transparent; color: var(--wp-text-dim); }
355
336
  .wp-ctxmenu__icon,
356
337
  .wp-ctxmenu__icon-spacer {
357
338
  width: 14px;
@@ -119,7 +119,7 @@ onBeforeUnmount(() => clearTimeout(timer));
119
119
  .wp-copy-btn[data-size="lg"] { min-width: var(--wp-control-h-lg); height: var(--wp-control-h-lg); }
120
120
  .wp-copy-btn[data-variant="secondary"] { background: var(--wp-bg-3); border-color: var(--wp-border); color: var(--wp-text); }
121
121
  .wp-copy-btn:not([data-inline]) .pi { font-size: 12px; }
122
- .wp-copy-btn[data-size="sm"]:not([data-inline]) .pi { font-size: 11px; }
122
+ .wp-copy-btn[data-size="sm"]:not([data-inline]) .pi { font-size: 12px; }
123
123
  .wp-copy-btn:not([data-inline]):hover:not(:disabled) { background: var(--wp-bg-3); border-color: var(--wp-border); color: var(--wp-text); }
124
124
  .wp-copy-btn[data-variant="secondary"]:hover:not(:disabled) { background: var(--wp-bg-4); border-color: var(--wp-border-strong); }
125
125
  .wp-copy-btn:not([data-inline]):focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
package/src/DataTable.vue CHANGED
@@ -165,7 +165,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
165
165
  <tr>
166
166
  <th v-if="selectable" class="wp-data-table__check" scope="col">
167
167
  <Checkbox
168
- class="wp-checkbox--inline"
169
168
  :model-value="allPicked"
170
169
  :indeterminate="somePicked"
171
170
  :disabled="loading || allKeys.length === 0"
@@ -227,7 +226,6 @@ const span = computed(() => props.columns.length + (props.selectable ? 1 : 0));
227
226
  >
228
227
  <td v-if="selectable" class="wp-data-table__check" @click.stop>
229
228
  <Checkbox
230
- class="wp-checkbox--inline"
231
229
  :model-value="picked.has(keyOf(row))"
232
230
  aria-label="Select row"
233
231
  @update:model-value="(on: boolean) => toggleRow(row, on)"
@@ -209,7 +209,7 @@ function clear(): void {
209
209
  padding: 0 var(--wp-space-2); border: 0; border-radius: var(--wp-radius-sm);
210
210
  background: transparent; color: var(--wp-text-muted); cursor: pointer; font: inherit;
211
211
  }
212
- .wp-date__nav .pi { font-size: 11px; }
212
+ .wp-date__nav .pi { font-size: 12px; }
213
213
  .wp-date__nav:hover, .wp-date__link:hover:not(:disabled) { background: var(--wp-row-hover); color: var(--wp-text); }
214
214
  .wp-date__nav:focus-visible, .wp-date__link:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--wp-border-focus); }
215
215
  .wp-date__link:disabled { opacity: 0.45; cursor: not-allowed; }
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ /** One derivation condition: a single test, or an ALL OF / ANY OF group
3
+ * drawn as a nested box (schema v7 AND / OR). */
4
+ import type { Condition } from "./preview-readers";
5
+
6
+ defineOptions({ name: "DerivationCondition" });
7
+ defineProps<{ cond: Condition }>();
8
+ </script>
9
+
10
+ <template>
11
+ <span
12
+ v-if="cond.kind === 'test'"
13
+ class="wp-dv-test"
14
+ data-test="derivation-test"
15
+ >
16
+ {{ cond.var }} <em>{{ cond.op.replace(/_/g, " ") }}</em>{{ cond.value !== null ? ` ${cond.value}` : "" }}
17
+ </span>
18
+ <div
19
+ v-else
20
+ class="wp-dv-group"
21
+ :data-match="cond.match"
22
+ data-test="derivation-group"
23
+ >
24
+ <span class="wp-dv-group__head">{{ cond.match === "any" ? "ANY OF" : "ALL OF" }}</span>
25
+ <DerivationCondition
26
+ v-for="(c, i) in cond.conditions"
27
+ :key="i"
28
+ :cond="c"
29
+ />
30
+ </div>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .wp-dv-test {
35
+ display: inline-block;
36
+ align-self: flex-start;
37
+ font-family: var(--wp-font-mono);
38
+ font-size: 12px;
39
+ padding: 2px 8px;
40
+ border-radius: min(5px, var(--wp-radius-cap, 999px));
41
+ background: var(--wp-bg-3);
42
+ color: var(--wp-text);
43
+ overflow-wrap: anywhere;
44
+ }
45
+ .wp-dv-test em {
46
+ font-style: normal;
47
+ color: var(--wp-text-muted);
48
+ }
49
+ .wp-dv-group {
50
+ display: inline-flex;
51
+ flex-direction: column;
52
+ gap: 5px;
53
+ max-width: 100%;
54
+ padding: 6px 8px;
55
+ border: 1px dashed var(--wp-border-strong);
56
+ border-radius: min(8px, var(--wp-radius-cap, 999px));
57
+ }
58
+ .wp-dv-group__head {
59
+ font-family: var(--wp-font-mono);
60
+ font-size: 9.5px;
61
+ font-weight: 700;
62
+ letter-spacing: 0.08em;
63
+ color: var(--wp-constraint-source-text);
64
+ }
65
+ .wp-dv-group[data-match="any"] > .wp-dv-group__head {
66
+ color: var(--wp-constraint-target-text);
67
+ }
68
+ </style>