@dumiflex/wp-kit 0.3.0 → 0.4.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 +77 -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 +119 -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 +292 -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 +125 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +173 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +175 -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 +82 -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/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/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
package/src/TokenizedValue.vue
CHANGED
|
@@ -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>
|