@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +92 -51
  2. package/LICENSE +674 -674
  3. package/README.md +73 -76
  4. package/package.json +78 -77
  5. package/src/Avatar.vue +83 -83
  6. package/src/Badge.vue +97 -97
  7. package/src/Banner.vue +118 -118
  8. package/src/BookmarkButton.vue +27 -27
  9. package/src/BundleMeta.vue +51 -51
  10. package/src/Button.vue +103 -103
  11. package/src/Card.vue +68 -68
  12. package/src/Checkbox.vue +109 -107
  13. package/src/Chip.vue +163 -97
  14. package/src/ChoiceCards.vue +133 -133
  15. package/src/CloseButton.vue +69 -69
  16. package/src/CodeBlock.vue +146 -119
  17. package/src/ColorField.vue +188 -206
  18. package/src/ColorPicker.vue +201 -201
  19. package/src/Combobox.vue +331 -334
  20. package/src/ConfirmDialog.vue +165 -163
  21. package/src/ConfirmModal.vue +147 -147
  22. package/src/ConstraintMatrixPreview.vue +708 -708
  23. package/src/ContextMenu.vue +386 -379
  24. package/src/CopyButton.vue +156 -156
  25. package/src/DataTable.vue +293 -292
  26. package/src/DatePicker.vue +234 -234
  27. package/src/DescriptionList.vue +73 -73
  28. package/src/DiffView.vue +68 -68
  29. package/src/Disclosure.vue +166 -166
  30. package/src/DragHandle.vue +49 -49
  31. package/src/Drawer.vue +131 -131
  32. package/src/DropZone.vue +196 -196
  33. package/src/DropdownMenu.vue +72 -72
  34. package/src/EmptyState.vue +111 -111
  35. package/src/Field.vue +28 -28
  36. package/src/HsvPicker.vue +208 -208
  37. package/src/Icon.vue +39 -39
  38. package/src/IconPicker.vue +209 -209
  39. package/src/ImageGrid.vue +127 -125
  40. package/src/InlineEdit.vue +183 -183
  41. package/src/Input.vue +99 -99
  42. package/src/Kbd.vue +56 -56
  43. package/src/Lightbox.vue +176 -173
  44. package/src/ListFilter.vue +85 -85
  45. package/src/ListRow.vue +204 -204
  46. package/src/LoadingMark.vue +217 -161
  47. package/src/Logo.vue +60 -60
  48. package/src/Modal.vue +78 -76
  49. package/src/ModalShell.vue +109 -107
  50. package/src/Pagination.vue +106 -106
  51. package/src/Popover.vue +180 -171
  52. package/src/PostCard.vue +210 -211
  53. package/src/ProgressBar.vue +90 -90
  54. package/src/RadioGroup.vue +177 -177
  55. package/src/RangeSlider.vue +183 -183
  56. package/src/RelativeDate.vue +35 -35
  57. package/src/ResizeHandle.vue +143 -143
  58. package/src/SearchInput.vue +192 -199
  59. package/src/Select.vue +644 -641
  60. package/src/Separator.vue +54 -54
  61. package/src/Skeleton.vue +98 -82
  62. package/src/Slider.vue +198 -198
  63. package/src/SortableList.vue +260 -260
  64. package/src/SplitPane.vue +68 -68
  65. package/src/StarButton.vue +40 -40
  66. package/src/Stepper.vue +141 -141
  67. package/src/SupportLinks.vue +93 -93
  68. package/src/TagInput.vue +297 -300
  69. package/src/Textarea.vue +57 -57
  70. package/src/Timeline.vue +92 -92
  71. package/src/ToastCard.vue +119 -119
  72. package/src/ToastHost.vue +40 -40
  73. package/src/Toggle.vue +36 -36
  74. package/src/TokenizedValue.vue +310 -310
  75. package/src/Toolbar.vue +110 -110
  76. package/src/VirtualList.vue +87 -87
  77. package/src/VisuallyHidden.vue +29 -29
  78. package/src/WpButton.vue +75 -75
  79. package/src/WpCheck.vue +104 -102
  80. package/src/WpIconButton.vue +107 -107
  81. package/src/WpImageZoom.vue +544 -544
  82. package/src/WpNumber.vue +178 -183
  83. package/src/WpSegmented.vue +175 -175
  84. package/src/WpSelect.vue +93 -89
  85. package/src/WpSwitch.vue +163 -163
  86. package/src/WpTabs.vue +188 -188
  87. package/src/_modal-motion.css +33 -33
  88. package/src/_skeleton.css +50 -50
  89. package/src/axis-color.ts +42 -42
  90. package/src/button.css +169 -169
  91. package/src/calendar.ts +59 -59
  92. package/src/clipboard.ts +50 -50
  93. package/src/colour-presets.ts +12 -12
  94. package/src/diff.ts +51 -51
  95. package/src/drop-zone.ts +41 -41
  96. package/src/field.css +50 -0
  97. package/src/flip.ts +237 -237
  98. package/src/floating-layer.ts +48 -0
  99. package/src/floating.ts +99 -99
  100. package/src/highlight.ts +199 -0
  101. package/src/hsv.ts +100 -100
  102. package/src/icon-catalog.ts +52 -52
  103. package/src/image-zoom.ts +93 -93
  104. package/src/kind-colour.ts +28 -0
  105. package/src/loading-mark-motion.ts +249 -0
  106. package/src/logo-paths.ts +21 -21
  107. package/src/matrix-axis.ts +91 -91
  108. package/src/pagination.ts +27 -27
  109. package/src/platform-keys.ts +29 -29
  110. package/src/rich-tokenize.ts +357 -357
  111. package/src/select-types.ts +32 -32
  112. package/src/slider-math.ts +45 -45
  113. package/src/sortable.ts +22 -22
  114. package/src/theme/fonts/OFL-Fraunces.txt +93 -93
  115. package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
  116. package/src/theme/fonts/OFL-Manrope.txt +93 -93
  117. package/src/theme/looks.css +428 -428
  118. package/src/theme/palettes.css +254 -254
  119. package/src/theme/tokens.css +2896 -2894
  120. package/src/tokens.ts +56 -56
  121. package/src/tooltip.css +43 -43
  122. package/src/tooltip.ts +146 -146
  123. package/src/useDialogFocus.ts +73 -73
  124. package/src/useGrowableField.ts +357 -357
  125. package/src/useModalKeyShield.ts +116 -116
  126. package/src/useToast.ts +48 -48
  127. package/src/wp-logo.svg +14 -14
  128. package/src/ChipToggle.vue +0 -92
  129. package/src/KindChip.vue +0 -53
  130. package/src/RatingBadge.vue +0 -42
@@ -1,310 +1,310 @@
1
- <script setup lang="ts">
2
- /**
3
- * Read-only renderer for a wildcard-grammar value string.
4
- *
5
- * Consumes the token stream from `tokenizeRich` (chunk A1) and paints each
6
- * token as a coloured inline span or chip — vars (green), refs (magenta),
7
- * brace alternations (amber scaffolding) and multi-picks (green scaffolding).
8
- * The component is RECURSIVE: brace arms re-enter `TokenizedValue` so a
9
- * `@{uuid}` or `$var` nested inside `{a|@{…}|$rank}` still renders as a chip.
10
- *
11
- * No `v-html` — every token becomes a `v-for`-rendered element (CLAUDE.md
12
- * confines raw-HTML rendering to `utils/sanitize.ts`). This mirrors the
13
- * sister project's RefChip + rich-text.css chrome (read, not imported — the
14
- * two repos diverge) but emits a flat, framework-local span tree.
15
- *
16
- * The token stream is pre-flattened in `<script>` into a single list of
17
- * `RenderItem`s (text / escape / var / ref / scaffolding-glyph / nested
18
- * child). That keeps the template a flat `v-for` — no nested branch loops —
19
- * which both reads cleanly and lets adjacent items abut with no stray
20
- * whitespace (tokens are contiguous; an inter-element space would corrupt
21
- * the rendered value).
22
- */
23
- import { computed } from "vue";
24
- import {
25
- splitRefFilter,
26
- tokenizeRich,
27
- type RichToken,
28
- } from "./rich-tokenize";
29
-
30
- defineOptions({ name: "TokenizedValue" });
31
-
32
- const props = withDefaults(
33
- defineProps<{
34
- /** The raw wildcard-grammar string to render. */
35
- value: string;
36
- /** uuid → display-name map for resolving `@{uuid}` refs. Empty by default
37
- * (callers without a catalog still render; refs fall back to #name/uuid). */
38
- uuidToName?: ReadonlyMap<string, string>;
39
- }>(),
40
- { uuidToName: () => new Map<string, string>() },
41
- );
42
-
43
- /** Concrete (never-undefined) map for binding into nested instances —
44
- * `withDefaults` guarantees a value, but the recursive self-reference's
45
- * optional prop type needs an explicitly non-optional source under
46
- * `exactOptionalPropertyTypes`. */
47
- const refMap = computed<ReadonlyMap<string, string>>(() => props.uuidToName);
48
-
49
- /** One drawable unit. A brace/multi token expands into several of these
50
- * (scaffolding glyphs interleaved with `child` arms); every other token
51
- * maps to exactly one. */
52
- type RenderItem =
53
- | { key: string; kind: "text" | "escape"; text: string }
54
- | { key: string; kind: "var"; label: string; neg: boolean }
55
- | { key: string; kind: "scaf"; text: string; flavor: "brace" | "multi" }
56
- | { key: string; kind: "child"; value: string }
57
- | {
58
- key: string;
59
- kind: "ref";
60
- uuid: string;
61
- label: string;
62
- resolved: boolean;
63
- expr: string;
64
- excludeNull: boolean;
65
- };
66
-
67
- /** Var chip label: `$name` plus an optional `.K` list accessor (SP2a) and
68
- * `.neg` negatives read (schema v8). */
69
- function varLabel(meta: RichToken["meta"]): string {
70
- const idx = meta?.index;
71
- return (
72
- "$" +
73
- (meta?.name ?? "") +
74
- (idx !== undefined ? "." + idx : "") +
75
- (meta?.neg ? ".neg" : "")
76
- );
77
- }
78
-
79
- /** Resolve a ref token into its chip view. Resolution precedence: live map
80
- * name → cached `#name` → bare uuid. The raw `:`-filter is peeled into an
81
- * expression + exclude-null flag (the expression rides in the hover title,
82
- * never inline, so long filters can't blow out the chip). */
83
- function refItem(token: RichToken, key: string): Extract<RenderItem, { kind: "ref" }> {
84
- const meta = token.meta ?? {};
85
- const uuid = meta.uuid ?? "";
86
- const mapped = props.uuidToName.get(uuid);
87
- const fromMap = typeof mapped === "string" && mapped.length > 0 ? mapped : "";
88
- const cached = typeof meta.name === "string" && meta.name.length > 0 ? meta.name : "";
89
- const resolvedName = fromMap || cached;
90
- const { expr, excludeNull } = splitRefFilter((meta.sub_categories ?? []).join(","));
91
- return {
92
- key,
93
- kind: "ref",
94
- uuid,
95
- label: resolvedName || uuid,
96
- resolved: resolvedName.length > 0,
97
- expr,
98
- excludeNull,
99
- };
100
- }
101
-
102
- /** Reconstruct a multi-pick's opening scaffolding from its meta: `{N`, `-M`
103
- * (range only), `~` (independent), then the `$$sep$$` delimiter. Inverse of
104
- * the tokenizer so a decomposed block reads identically to its source. */
105
- function multiPrefix(meta: RichToken["meta"]): string {
106
- const min = meta?.min ?? 0;
107
- const max = meta?.max ?? min;
108
- const range = max !== min ? "-" + max : "";
109
- const indep = meta?.independent ? "~" : "";
110
- return "{" + min + range + indep + "$$" + (meta?.sep ?? "") + "$$";
111
- }
112
-
113
- /** Flatten a brace/multi token into scaffolding glyphs + child arms. */
114
- function expandBlock(
115
- token: RichToken,
116
- flavor: "brace" | "multi",
117
- open: string,
118
- base: string,
119
- ): RenderItem[] {
120
- const out: RenderItem[] = [];
121
- const branches = token.meta?.branches ?? [];
122
- out.push({ key: base + "-o", kind: "scaf", text: open, flavor });
123
- branches.forEach((branch, bi) => {
124
- if (bi > 0) out.push({ key: `${base}-p${bi}`, kind: "scaf", text: "|", flavor });
125
- out.push({ key: `${base}-c${bi}`, kind: "child", value: branch });
126
- });
127
- out.push({ key: base + "-x", kind: "scaf", text: "}", flavor });
128
- return out;
129
- }
130
-
131
- const items = computed<RenderItem[]>(() => {
132
- const out: RenderItem[] = [];
133
- for (const token of tokenizeRich(props.value)) {
134
- const key = String(token.start);
135
- switch (token.kind) {
136
- case "text":
137
- out.push({ key, kind: "text", text: token.raw });
138
- break;
139
- case "escape":
140
- out.push({ key, kind: "escape", text: token.raw });
141
- break;
142
- case "var":
143
- out.push({ key, kind: "var", label: varLabel(token.meta), neg: token.meta?.neg === true });
144
- break;
145
- case "ref":
146
- out.push(refItem(token, key));
147
- break;
148
- case "dp-brace":
149
- out.push(...expandBlock(token, "brace", "{", key));
150
- break;
151
- case "dp-multi":
152
- out.push(...expandBlock(token, "multi", multiPrefix(token.meta), key));
153
- break;
154
- default:
155
- // Defensive: any future/unhandled kind still shows its raw text so
156
- // the lossless invariant holds visually.
157
- out.push({ key, kind: "text", text: token.raw });
158
- }
159
- }
160
- return out;
161
- });
162
- </script>
163
-
164
- <template>
165
- <span class="wp-tv">
166
- <template
167
- v-for="item in items"
168
- :key="item.key"
169
- >
170
- <span
171
- v-if="item.kind === 'text'"
172
- class="wp-tv-text"
173
- >{{ item.text }}</span>
174
- <span
175
- v-else-if="item.kind === 'escape'"
176
- class="wp-tv-escape"
177
- >{{ item.text }}</span>
178
- <span
179
- v-else-if="item.kind === 'var'"
180
- class="wp-tv-var"
181
- :data-neg="item.neg || null"
182
- >{{ item.label }}</span>
183
- <span
184
- v-else-if="item.kind === 'scaf'"
185
- class="wp-tv-scaf"
186
- :class="item.flavor === 'multi' ? 'wp-tv-scaf--multi' : 'wp-tv-scaf--brace'"
187
- >{{ item.text }}</span>
188
- <TokenizedValue
189
- v-else-if="item.kind === 'child'"
190
- :value="item.value"
191
- :uuid-to-name="refMap"
192
- />
193
- <span
194
- v-else-if="item.kind === 'ref'"
195
- class="wp-tv-ref"
196
- :class="{ 'wp-tv-ref--unresolved': !item.resolved }"
197
- :title="item.expr || undefined"
198
- >
199
- <i
200
- class="pi wp-tv-ref__icon"
201
- :class="item.resolved ? 'pi-sparkles' : 'pi-question-circle'"
202
- aria-hidden="true"
203
- />
204
- <span class="wp-tv-ref__label">{{ item.label }}</span>
205
- <i
206
- v-if="item.expr"
207
- class="pi pi-filter wp-tv-ref__funnel"
208
- aria-hidden="true"
209
- />
210
- <i
211
- v-if="item.excludeNull"
212
- class="pi pi-ban wp-tv-ref__nonull"
213
- aria-hidden="true"
214
- />
215
- </span>
216
- </template>
217
- </span>
218
- </template>
219
-
220
- <style scoped>
221
- .wp-tv {
222
- /* Host text size — chips deliberately read SMALLER than this line. */
223
- font-size: 13px;
224
- line-height: 1.6;
225
- word-break: break-word;
226
- }
227
-
228
- .wp-tv-text {
229
- color: var(--wp-text);
230
- }
231
-
232
- .wp-tv-escape {
233
- color: var(--wp-text-dim);
234
- }
235
-
236
- /* ── Chips (var + ref) ───────────────────────────────────────────────
237
- * Inline (not inline-flex) so they wrap with the surrounding prose and
238
- * sit on the text baseline. 11px < the 13px host line keeps them from
239
- * towering over the text. */
240
- .wp-tv-var,
241
- .wp-tv-ref {
242
- display: inline;
243
- font-size: 11px;
244
- padding: 1px 4px;
245
- border-radius: min(4px, var(--wp-radius-cap, 999px));
246
- white-space: nowrap;
247
- vertical-align: baseline;
248
- }
249
-
250
- /* $var — green. */
251
- .wp-tv-var {
252
- background: color-mix(in oklab, var(--wp-success) 16%, transparent);
253
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-success) 50%, transparent);
254
- color: var(--wp-success);
255
- font-weight: 500;
256
- }
257
- /* $var.neg — reads the variable's negatives, so it wears the negative red. */
258
- .wp-tv-var[data-neg] {
259
- background: color-mix(in oklab, var(--wp-danger) 14%, transparent);
260
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-danger) 45%, transparent);
261
- color: var(--wp-danger-text, var(--wp-danger));
262
- }
263
-
264
- /* @{uuid} ref — magenta. Carries a hover title (the filter), so `help`. */
265
- .wp-tv-ref {
266
- background: color-mix(in oklab, var(--wp-kind-ref) 15%, transparent);
267
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-kind-ref) 50%, transparent);
268
- color: var(--wp-kind-ref);
269
- font-weight: 500;
270
- cursor: help;
271
- }
272
-
273
- /* Unresolved ref — red. Overrides the magenta tone above. */
274
- .wp-tv-ref--unresolved {
275
- background: color-mix(in oklab, var(--wp-danger) 15%, transparent);
276
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-danger) 50%, transparent);
277
- color: var(--wp-danger);
278
- }
279
-
280
- /* In-chip PrimeIcons — small + baseline-nudged so they don't lift the line. */
281
- .wp-tv-ref__icon,
282
- .wp-tv-ref__funnel,
283
- .wp-tv-ref__nonull {
284
- font-size: 8px;
285
- vertical-align: 0;
286
- }
287
- .wp-tv-ref__icon {
288
- margin-right: 3px;
289
- }
290
- .wp-tv-ref__funnel {
291
- margin-left: 3px;
292
- color: var(--wp-warn);
293
- }
294
- .wp-tv-ref__nonull {
295
- margin-left: 2px;
296
- color: var(--wp-text-dim);
297
- }
298
-
299
- /* ── Brace / multi scaffolding ───────────────────────────────────────
300
- * Colour-only (no background, no box) — the bordered chips are the nested
301
- * arms. Brace = amber, multi = green. */
302
- .wp-tv-scaf--brace {
303
- color: var(--wp-warn);
304
- font-weight: 600;
305
- }
306
- .wp-tv-scaf--multi {
307
- color: var(--wp-kind-derivation);
308
- font-weight: 600;
309
- }
310
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * Read-only renderer for a wildcard-grammar value string.
4
+ *
5
+ * Consumes the token stream from `tokenizeRich` (chunk A1) and paints each
6
+ * token as a coloured inline span or chip — vars (green), refs (magenta),
7
+ * brace alternations (amber scaffolding) and multi-picks (green scaffolding).
8
+ * The component is RECURSIVE: brace arms re-enter `TokenizedValue` so a
9
+ * `@{uuid}` or `$var` nested inside `{a|@{…}|$rank}` still renders as a chip.
10
+ *
11
+ * No `v-html` — every token becomes a `v-for`-rendered element (CLAUDE.md
12
+ * confines raw-HTML rendering to `utils/sanitize.ts`). This mirrors the
13
+ * sister project's RefChip + rich-text.css chrome (read, not imported — the
14
+ * two repos diverge) but emits a flat, framework-local span tree.
15
+ *
16
+ * The token stream is pre-flattened in `<script>` into a single list of
17
+ * `RenderItem`s (text / escape / var / ref / scaffolding-glyph / nested
18
+ * child). That keeps the template a flat `v-for` — no nested branch loops —
19
+ * which both reads cleanly and lets adjacent items abut with no stray
20
+ * whitespace (tokens are contiguous; an inter-element space would corrupt
21
+ * the rendered value).
22
+ */
23
+ import { computed } from "vue";
24
+ import {
25
+ splitRefFilter,
26
+ tokenizeRich,
27
+ type RichToken,
28
+ } from "./rich-tokenize";
29
+
30
+ defineOptions({ name: "TokenizedValue" });
31
+
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ /** The raw wildcard-grammar string to render. */
35
+ value: string;
36
+ /** uuid → display-name map for resolving `@{uuid}` refs. Empty by default
37
+ * (callers without a catalog still render; refs fall back to #name/uuid). */
38
+ uuidToName?: ReadonlyMap<string, string>;
39
+ }>(),
40
+ { uuidToName: () => new Map<string, string>() },
41
+ );
42
+
43
+ /** Concrete (never-undefined) map for binding into nested instances —
44
+ * `withDefaults` guarantees a value, but the recursive self-reference's
45
+ * optional prop type needs an explicitly non-optional source under
46
+ * `exactOptionalPropertyTypes`. */
47
+ const refMap = computed<ReadonlyMap<string, string>>(() => props.uuidToName);
48
+
49
+ /** One drawable unit. A brace/multi token expands into several of these
50
+ * (scaffolding glyphs interleaved with `child` arms); every other token
51
+ * maps to exactly one. */
52
+ type RenderItem =
53
+ | { key: string; kind: "text" | "escape"; text: string }
54
+ | { key: string; kind: "var"; label: string; neg: boolean }
55
+ | { key: string; kind: "scaf"; text: string; flavor: "brace" | "multi" }
56
+ | { key: string; kind: "child"; value: string }
57
+ | {
58
+ key: string;
59
+ kind: "ref";
60
+ uuid: string;
61
+ label: string;
62
+ resolved: boolean;
63
+ expr: string;
64
+ excludeNull: boolean;
65
+ };
66
+
67
+ /** Var chip label: `$name` plus an optional `.K` list accessor (SP2a) and
68
+ * `.neg` negatives read (schema v8). */
69
+ function varLabel(meta: RichToken["meta"]): string {
70
+ const idx = meta?.index;
71
+ return (
72
+ "$" +
73
+ (meta?.name ?? "") +
74
+ (idx !== undefined ? "." + idx : "") +
75
+ (meta?.neg ? ".neg" : "")
76
+ );
77
+ }
78
+
79
+ /** Resolve a ref token into its chip view. Resolution precedence: live map
80
+ * name → cached `#name` → bare uuid. The raw `:`-filter is peeled into an
81
+ * expression + exclude-null flag (the expression rides in the hover title,
82
+ * never inline, so long filters can't blow out the chip). */
83
+ function refItem(token: RichToken, key: string): Extract<RenderItem, { kind: "ref" }> {
84
+ const meta = token.meta ?? {};
85
+ const uuid = meta.uuid ?? "";
86
+ const mapped = props.uuidToName.get(uuid);
87
+ const fromMap = typeof mapped === "string" && mapped.length > 0 ? mapped : "";
88
+ const cached = typeof meta.name === "string" && meta.name.length > 0 ? meta.name : "";
89
+ const resolvedName = fromMap || cached;
90
+ const { expr, excludeNull } = splitRefFilter((meta.sub_categories ?? []).join(","));
91
+ return {
92
+ key,
93
+ kind: "ref",
94
+ uuid,
95
+ label: resolvedName || uuid,
96
+ resolved: resolvedName.length > 0,
97
+ expr,
98
+ excludeNull,
99
+ };
100
+ }
101
+
102
+ /** Reconstruct a multi-pick's opening scaffolding from its meta: `{N`, `-M`
103
+ * (range only), `~` (independent), then the `$$sep$$` delimiter. Inverse of
104
+ * the tokenizer so a decomposed block reads identically to its source. */
105
+ function multiPrefix(meta: RichToken["meta"]): string {
106
+ const min = meta?.min ?? 0;
107
+ const max = meta?.max ?? min;
108
+ const range = max !== min ? "-" + max : "";
109
+ const indep = meta?.independent ? "~" : "";
110
+ return "{" + min + range + indep + "$$" + (meta?.sep ?? "") + "$$";
111
+ }
112
+
113
+ /** Flatten a brace/multi token into scaffolding glyphs + child arms. */
114
+ function expandBlock(
115
+ token: RichToken,
116
+ flavor: "brace" | "multi",
117
+ open: string,
118
+ base: string,
119
+ ): RenderItem[] {
120
+ const out: RenderItem[] = [];
121
+ const branches = token.meta?.branches ?? [];
122
+ out.push({ key: base + "-o", kind: "scaf", text: open, flavor });
123
+ branches.forEach((branch, bi) => {
124
+ if (bi > 0) out.push({ key: `${base}-p${bi}`, kind: "scaf", text: "|", flavor });
125
+ out.push({ key: `${base}-c${bi}`, kind: "child", value: branch });
126
+ });
127
+ out.push({ key: base + "-x", kind: "scaf", text: "}", flavor });
128
+ return out;
129
+ }
130
+
131
+ const items = computed<RenderItem[]>(() => {
132
+ const out: RenderItem[] = [];
133
+ for (const token of tokenizeRich(props.value)) {
134
+ const key = String(token.start);
135
+ switch (token.kind) {
136
+ case "text":
137
+ out.push({ key, kind: "text", text: token.raw });
138
+ break;
139
+ case "escape":
140
+ out.push({ key, kind: "escape", text: token.raw });
141
+ break;
142
+ case "var":
143
+ out.push({ key, kind: "var", label: varLabel(token.meta), neg: token.meta?.neg === true });
144
+ break;
145
+ case "ref":
146
+ out.push(refItem(token, key));
147
+ break;
148
+ case "dp-brace":
149
+ out.push(...expandBlock(token, "brace", "{", key));
150
+ break;
151
+ case "dp-multi":
152
+ out.push(...expandBlock(token, "multi", multiPrefix(token.meta), key));
153
+ break;
154
+ default:
155
+ // Defensive: any future/unhandled kind still shows its raw text so
156
+ // the lossless invariant holds visually.
157
+ out.push({ key, kind: "text", text: token.raw });
158
+ }
159
+ }
160
+ return out;
161
+ });
162
+ </script>
163
+
164
+ <template>
165
+ <span class="wp-tv">
166
+ <template
167
+ v-for="item in items"
168
+ :key="item.key"
169
+ >
170
+ <span
171
+ v-if="item.kind === 'text'"
172
+ class="wp-tv-text"
173
+ >{{ item.text }}</span>
174
+ <span
175
+ v-else-if="item.kind === 'escape'"
176
+ class="wp-tv-escape"
177
+ >{{ item.text }}</span>
178
+ <span
179
+ v-else-if="item.kind === 'var'"
180
+ class="wp-tv-var"
181
+ :data-neg="item.neg || null"
182
+ >{{ item.label }}</span>
183
+ <span
184
+ v-else-if="item.kind === 'scaf'"
185
+ class="wp-tv-scaf"
186
+ :class="item.flavor === 'multi' ? 'wp-tv-scaf--multi' : 'wp-tv-scaf--brace'"
187
+ >{{ item.text }}</span>
188
+ <TokenizedValue
189
+ v-else-if="item.kind === 'child'"
190
+ :value="item.value"
191
+ :uuid-to-name="refMap"
192
+ />
193
+ <span
194
+ v-else-if="item.kind === 'ref'"
195
+ class="wp-tv-ref"
196
+ :class="{ 'wp-tv-ref--unresolved': !item.resolved }"
197
+ :title="item.expr || undefined"
198
+ >
199
+ <i
200
+ class="pi wp-tv-ref__icon"
201
+ :class="item.resolved ? 'pi-sparkles' : 'pi-question-circle'"
202
+ aria-hidden="true"
203
+ />
204
+ <span class="wp-tv-ref__label">{{ item.label }}</span>
205
+ <i
206
+ v-if="item.expr"
207
+ class="pi pi-filter wp-tv-ref__funnel"
208
+ aria-hidden="true"
209
+ />
210
+ <i
211
+ v-if="item.excludeNull"
212
+ class="pi pi-ban wp-tv-ref__nonull"
213
+ aria-hidden="true"
214
+ />
215
+ </span>
216
+ </template>
217
+ </span>
218
+ </template>
219
+
220
+ <style scoped>
221
+ .wp-tv {
222
+ /* Host text size — chips deliberately read SMALLER than this line. */
223
+ font-size: 13px;
224
+ line-height: 1.6;
225
+ word-break: break-word;
226
+ }
227
+
228
+ .wp-tv-text {
229
+ color: var(--wp-text);
230
+ }
231
+
232
+ .wp-tv-escape {
233
+ color: var(--wp-text-dim);
234
+ }
235
+
236
+ /* ── Chips (var + ref) ───────────────────────────────────────────────
237
+ * Inline (not inline-flex) so they wrap with the surrounding prose and
238
+ * sit on the text baseline. 11px < the 13px host line keeps them from
239
+ * towering over the text. */
240
+ .wp-tv-var,
241
+ .wp-tv-ref {
242
+ display: inline;
243
+ font-size: 11px;
244
+ padding: 1px 4px;
245
+ border-radius: min(4px, var(--wp-radius-cap, 999px));
246
+ white-space: nowrap;
247
+ vertical-align: baseline;
248
+ }
249
+
250
+ /* $var — green. */
251
+ .wp-tv-var {
252
+ background: color-mix(in oklab, var(--wp-success) 16%, transparent);
253
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-success) 50%, transparent);
254
+ color: var(--wp-success);
255
+ font-weight: 500;
256
+ }
257
+ /* $var.neg — reads the variable's negatives, so it wears the negative red. */
258
+ .wp-tv-var[data-neg] {
259
+ background: color-mix(in oklab, var(--wp-danger) 14%, transparent);
260
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-danger) 45%, transparent);
261
+ color: var(--wp-danger-text, var(--wp-danger));
262
+ }
263
+
264
+ /* @{uuid} ref — magenta. Carries a hover title (the filter), so `help`. */
265
+ .wp-tv-ref {
266
+ background: color-mix(in oklab, var(--wp-kind-ref) 15%, transparent);
267
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-kind-ref) 50%, transparent);
268
+ color: var(--wp-kind-ref);
269
+ font-weight: 500;
270
+ cursor: help;
271
+ }
272
+
273
+ /* Unresolved ref — red. Overrides the magenta tone above. */
274
+ .wp-tv-ref--unresolved {
275
+ background: color-mix(in oklab, var(--wp-danger) 15%, transparent);
276
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wp-danger) 50%, transparent);
277
+ color: var(--wp-danger);
278
+ }
279
+
280
+ /* In-chip PrimeIcons — small + baseline-nudged so they don't lift the line. */
281
+ .wp-tv-ref__icon,
282
+ .wp-tv-ref__funnel,
283
+ .wp-tv-ref__nonull {
284
+ font-size: 8px;
285
+ vertical-align: 0;
286
+ }
287
+ .wp-tv-ref__icon {
288
+ margin-right: 3px;
289
+ }
290
+ .wp-tv-ref__funnel {
291
+ margin-left: 3px;
292
+ color: var(--wp-warn);
293
+ }
294
+ .wp-tv-ref__nonull {
295
+ margin-left: 2px;
296
+ color: var(--wp-text-dim);
297
+ }
298
+
299
+ /* ── Brace / multi scaffolding ───────────────────────────────────────
300
+ * Colour-only (no background, no box) — the bordered chips are the nested
301
+ * arms. Brace = amber, multi = green. */
302
+ .wp-tv-scaf--brace {
303
+ color: var(--wp-warn);
304
+ font-weight: 600;
305
+ }
306
+ .wp-tv-scaf--multi {
307
+ color: var(--wp-kind-derivation);
308
+ font-weight: 600;
309
+ }
310
+ </style>