@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.
- package/CHANGELOG.md +47 -0
- package/README.md +5 -0
- package/package.json +4 -3
- package/src/Avatar.vue +1 -1
- package/src/Banner.vue +4 -4
- package/src/BundlePreview.vue +254 -0
- package/src/Button.vue +73 -44
- package/src/Checkbox.vue +201 -63
- package/src/Chip.vue +5 -2
- package/src/CodeBlock.vue +1 -1
- package/src/ColorField.vue +60 -6
- package/src/CombinePreview.vue +150 -0
- package/src/Combobox.vue +10 -14
- package/src/ConfirmModal.vue +12 -22
- package/src/ConstraintMatrixPreview.vue +6 -22
- package/src/ConstraintPreview.vue +259 -0
- package/src/ContextMenu.vue +11 -30
- package/src/CopyButton.vue +1 -1
- package/src/DataTable.vue +0 -2
- package/src/DatePicker.vue +1 -1
- package/src/DerivationCondition.vue +68 -0
- package/src/DerivationPreview.vue +345 -0
- package/src/Disclosure.vue +2 -1
- package/src/Drawer.vue +2 -2
- package/src/DropZone.vue +2 -2
- package/src/FixedValuesPreview.vue +184 -0
- package/src/HsvPicker.vue +2 -2
- package/src/IconPicker.vue +32 -103
- package/src/IconPickerPanel.vue +132 -0
- package/src/ImageGrid.vue +2 -2
- package/src/Lightbox.vue +2 -2
- package/src/LoadingMark.vue +6 -61
- package/src/Modal.vue +109 -44
- package/src/ModalShell.vue +26 -5
- package/src/Pagination.vue +4 -4
- package/src/PayloadPreview.vue +154 -0
- package/src/Popover.vue +68 -12
- package/src/PostDetailSkeleton.vue +65 -0
- package/src/PostGridSkeleton.vue +61 -0
- package/src/PreviewGroup.vue +75 -0
- package/src/ProfileSkeleton.vue +73 -0
- package/src/RadioGroup.vue +5 -4
- package/src/RichText.vue +61 -0
- package/src/SearchInput.vue +1 -1
- package/src/Select.vue +53 -158
- package/src/SelectionBar.vue +90 -0
- package/src/SettingGroup.vue +57 -0
- package/src/SettingRow.vue +127 -0
- package/src/Skeleton.vue +1 -1
- package/src/Slider.vue +166 -55
- package/src/SortableList.vue +1 -1
- package/src/TagInput.vue +1 -1
- package/src/Timeline.vue +2 -2
- package/src/WildcardPreview.vue +607 -0
- package/src/WorkingLine.vue +234 -0
- package/src/WpIconButton.vue +3 -2
- package/src/WpImageZoom.vue +7 -7
- package/src/WpSegmented.vue +3 -1
- package/src/WpSwitch.vue +1 -1
- package/src/WpTabs.vue +1 -1
- package/src/_modal-motion.css +1 -1
- package/src/button.css +8 -6
- package/src/canvas-motion.ts +251 -0
- package/src/floating.ts +5 -0
- package/src/hsv.ts +1 -1
- package/src/kind-colour.ts +24 -1
- package/src/linkify.ts +142 -0
- package/src/loading-mark-motion.ts +12 -155
- package/src/matrix-axis.ts +28 -0
- package/src/menu.css +51 -0
- package/src/motion.css +73 -0
- package/src/pi-icons.ts +51 -0
- package/src/preview-readers.ts +401 -0
- package/src/preview.css +194 -0
- package/src/slider-math.ts +45 -1
- package/src/theme/looks.css +6 -19
- package/src/theme/tokens.css +12 -1108
- package/src/tokens.ts +15 -4
- package/src/tooltip.css +8 -5
- package/src/useDialogFocus.ts +11 -2
- package/src/working-line-motion.ts +135 -0
- package/src/CloseButton.vue +0 -69
- package/src/ColorPicker.vue +0 -201
- package/src/ConfirmDialog.vue +0 -165
- package/src/RangeSlider.vue +0 -183
- package/src/Toggle.vue +0 -36
- package/src/WpButton.vue +0 -75
- package/src/WpCheck.vue +0 -104
- package/src/_skeleton.css +0 -50
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive readers + parsers for published payloads. Pure (no Vue), shared
|
|
3
|
+
* by the per-type previews, the post header's feature pills and the bundle
|
|
4
|
+
* summary. A published payload is untrusted JSON, so every read tolerates a
|
|
5
|
+
* missing or mistyped field.
|
|
6
|
+
*/
|
|
7
|
+
import { tokenizeRich, type RichToken } from "./rich-tokenize";
|
|
8
|
+
|
|
9
|
+
export type Obj = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export function readString(obj: unknown, key: string): string | null {
|
|
12
|
+
if (!obj || typeof obj !== "object") return null;
|
|
13
|
+
const v = (obj as Obj)[key];
|
|
14
|
+
return typeof v === "string" ? v : null;
|
|
15
|
+
}
|
|
16
|
+
export function readNumber(obj: unknown, key: string): number | null {
|
|
17
|
+
if (!obj || typeof obj !== "object") return null;
|
|
18
|
+
const v = (obj as Obj)[key];
|
|
19
|
+
return typeof v === "number" ? v : null;
|
|
20
|
+
}
|
|
21
|
+
export function readArray(obj: unknown, key: string): unknown[] {
|
|
22
|
+
if (!obj || typeof obj !== "object") return [];
|
|
23
|
+
const v = (obj as Obj)[key];
|
|
24
|
+
return Array.isArray(v) ? v : [];
|
|
25
|
+
}
|
|
26
|
+
export function readObject(obj: unknown, key: string): Obj | null {
|
|
27
|
+
if (!obj || typeof obj !== "object") return null;
|
|
28
|
+
const v = (obj as Obj)[key];
|
|
29
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return null;
|
|
30
|
+
return v as Obj;
|
|
31
|
+
}
|
|
32
|
+
export function readStrings(obj: unknown, key: string): string[] {
|
|
33
|
+
return readArray(obj, key).filter((s): s is string => typeof s === "string");
|
|
34
|
+
}
|
|
35
|
+
/** Coerce a payload field into an axis map (`{axis: tag[]}`), keeping only
|
|
36
|
+
* string members. Null when absent or empty. */
|
|
37
|
+
export function readGroups(obj: unknown, key: string): Record<string, string[]> | null {
|
|
38
|
+
const raw = readObject(obj, key);
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
const out: Record<string, string[]> = {};
|
|
41
|
+
for (const [axis, members] of Object.entries(raw)) {
|
|
42
|
+
if (Array.isArray(members)) {
|
|
43
|
+
const tags = members.filter((m): m is string => typeof m === "string");
|
|
44
|
+
if (tags.length) out[axis] = tags;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Object.keys(out).length ? out : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Engine rows nest type fields under `payload`; older flat seeds don't. */
|
|
51
|
+
export function innerOf(payload: unknown): Obj | null {
|
|
52
|
+
return readObject(payload, "payload") ?? (payload && typeof payload === "object" ? (payload as Obj) : null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Module type, or "bundle" for a bundle row (no `type`, has `children`). */
|
|
56
|
+
export function payloadKind(payload: unknown): string | null {
|
|
57
|
+
return readString(payload, "type") ?? (readArray(payload, "children").length ? "bundle" : null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─────────── grammar scanning ───────────
|
|
61
|
+
|
|
62
|
+
/** Every token in `text`, brace/multi arms included (depth-first). */
|
|
63
|
+
function allTokens(text: string): RichToken[] {
|
|
64
|
+
const out: RichToken[] = [];
|
|
65
|
+
const walk = (s: string): void => {
|
|
66
|
+
for (const t of tokenizeRich(s)) {
|
|
67
|
+
out.push(t);
|
|
68
|
+
for (const b of t.meta?.branches ?? []) walk(b);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walk(text);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export interface TextScan {
|
|
75
|
+
vars: string[];
|
|
76
|
+
negVars: string[];
|
|
77
|
+
refs: number;
|
|
78
|
+
picks: number;
|
|
79
|
+
}
|
|
80
|
+
export function scanText(text: string): TextScan {
|
|
81
|
+
const vars: string[] = [];
|
|
82
|
+
const negVars: string[] = [];
|
|
83
|
+
let refs = 0;
|
|
84
|
+
let picks = 0;
|
|
85
|
+
for (const t of allTokens(text)) {
|
|
86
|
+
if (t.kind === "var" && t.meta?.name) {
|
|
87
|
+
(t.meta.neg ? negVars : vars).push(t.meta.name);
|
|
88
|
+
} else if (t.kind === "ref") refs++;
|
|
89
|
+
else if (t.kind === "dp-brace" || t.kind === "dp-multi") picks++;
|
|
90
|
+
}
|
|
91
|
+
return { vars, negVars, refs, picks };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────── wildcard ───────────
|
|
95
|
+
|
|
96
|
+
export interface WildcardOption {
|
|
97
|
+
value: string;
|
|
98
|
+
weight: number;
|
|
99
|
+
/** Plural tag list (a legacy singular `sub_category` folds in). */
|
|
100
|
+
tags: string[];
|
|
101
|
+
negative: string;
|
|
102
|
+
/** Schema v9: rolls normally, and is also used when nothing else is left to pick. */
|
|
103
|
+
fallback: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface WildcardAxis {
|
|
106
|
+
name: string;
|
|
107
|
+
tags: string[];
|
|
108
|
+
}
|
|
109
|
+
export function wildcardOptions(inner: unknown): WildcardOption[] {
|
|
110
|
+
return readArray(inner, "options")
|
|
111
|
+
.map((r) => {
|
|
112
|
+
const tags = readStrings(r, "sub_categories");
|
|
113
|
+
const singular = readString(r, "sub_category");
|
|
114
|
+
return {
|
|
115
|
+
value: readString(r, "value") ?? "",
|
|
116
|
+
weight: readNumber(r, "weight") ?? 1,
|
|
117
|
+
tags: tags.length ? tags : singular ? [singular] : [],
|
|
118
|
+
negative: readString(r, "negative") ?? "",
|
|
119
|
+
fallback: !!r && typeof r === "object" && (r as Obj).fallback === true,
|
|
120
|
+
};
|
|
121
|
+
})
|
|
122
|
+
.filter((o) => o.value);
|
|
123
|
+
}
|
|
124
|
+
/** Axes from `tag_groups`; without them, one flat axis over the
|
|
125
|
+
* `sub_categories` registry (or none at all). */
|
|
126
|
+
export function wildcardAxes(inner: unknown): WildcardAxis[] {
|
|
127
|
+
const groups = readGroups(inner, "tag_groups");
|
|
128
|
+
if (groups) return Object.entries(groups).map(([name, tags]) => ({ name, tags }));
|
|
129
|
+
const flat = readStrings(inner, "sub_categories");
|
|
130
|
+
return flat.length ? [{ name: "tag", tags: flat }] : [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─────────── fixed values / combine ───────────
|
|
134
|
+
|
|
135
|
+
export interface FixedValue {
|
|
136
|
+
name: string;
|
|
137
|
+
value: string;
|
|
138
|
+
negative: string;
|
|
139
|
+
}
|
|
140
|
+
export function fixedValues(inner: unknown): FixedValue[] {
|
|
141
|
+
// Engine shape is `values: [{id, name, value}]`; `entries/variable_name`
|
|
142
|
+
// was a stale community guess kept only for old published data.
|
|
143
|
+
const rows = readArray(inner, "values").length ? readArray(inner, "values") : readArray(inner, "entries");
|
|
144
|
+
return rows.map((r) => ({
|
|
145
|
+
name: readString(r, "name") ?? readString(r, "variable_name") ?? "",
|
|
146
|
+
value: readString(r, "value") ?? "",
|
|
147
|
+
negative: readString(r, "negative") ?? "",
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─────────── derivation ───────────
|
|
152
|
+
|
|
153
|
+
export type ActionOp = "set" | "append" | "prepend" | "neg_add" | "neg_replace" | "other";
|
|
154
|
+
export interface DerivationAction {
|
|
155
|
+
target: string;
|
|
156
|
+
op: ActionOp;
|
|
157
|
+
/** Raw mode string, shown when the op is unknown. */
|
|
158
|
+
mode: string;
|
|
159
|
+
value: string;
|
|
160
|
+
}
|
|
161
|
+
export interface CondTest {
|
|
162
|
+
kind: "test";
|
|
163
|
+
var: string;
|
|
164
|
+
op: string;
|
|
165
|
+
value: string | null;
|
|
166
|
+
}
|
|
167
|
+
export interface CondGroup {
|
|
168
|
+
kind: "group";
|
|
169
|
+
match: "all" | "any";
|
|
170
|
+
conditions: Condition[];
|
|
171
|
+
}
|
|
172
|
+
export type Condition = CondTest | CondGroup;
|
|
173
|
+
export interface DerivationBranch {
|
|
174
|
+
condition: Condition | null;
|
|
175
|
+
actions: DerivationAction[];
|
|
176
|
+
}
|
|
177
|
+
export interface DerivationRule {
|
|
178
|
+
branches: DerivationBranch[];
|
|
179
|
+
fallback: DerivationAction[] | null;
|
|
180
|
+
}
|
|
181
|
+
/** Presence checks carry no value. */
|
|
182
|
+
const VALUELESS_OPS = new Set(["exists", "not_exists", "is_empty", "is_not_empty"]);
|
|
183
|
+
|
|
184
|
+
export function parseCondition(cond: unknown): Condition | null {
|
|
185
|
+
if (!cond || typeof cond !== "object") return null;
|
|
186
|
+
if ("conditions" in cond) {
|
|
187
|
+
const kids = readArray(cond, "conditions")
|
|
188
|
+
.map(parseCondition)
|
|
189
|
+
.filter((c): c is Condition => c !== null);
|
|
190
|
+
if (!kids.length) return null;
|
|
191
|
+
return { kind: "group", match: readString(cond, "match") === "any" ? "any" : "all", conditions: kids };
|
|
192
|
+
}
|
|
193
|
+
const op = readString(cond, "op") ?? "?";
|
|
194
|
+
const raw = (cond as Obj)["value"];
|
|
195
|
+
return {
|
|
196
|
+
kind: "test",
|
|
197
|
+
var: readString(cond, "var") ?? "?",
|
|
198
|
+
op,
|
|
199
|
+
value: VALUELESS_OPS.has(op) ? null : typeof raw === "string" ? raw : JSON.stringify(raw ?? ""),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function parseAction(action: unknown): DerivationAction {
|
|
203
|
+
const mode = readString(action, "mode") ?? "?";
|
|
204
|
+
const raw = (action as Obj | null)?.["value"];
|
|
205
|
+
const op: ActionOp =
|
|
206
|
+
mode === "replace"
|
|
207
|
+
? "set"
|
|
208
|
+
: mode === "append"
|
|
209
|
+
? "append"
|
|
210
|
+
: mode === "prepend"
|
|
211
|
+
? "prepend"
|
|
212
|
+
: mode === "negative"
|
|
213
|
+
? "neg_add"
|
|
214
|
+
: mode === "negative_replace"
|
|
215
|
+
? "neg_replace"
|
|
216
|
+
: "other";
|
|
217
|
+
return {
|
|
218
|
+
target: readString(action, "target_var") ?? "?",
|
|
219
|
+
op,
|
|
220
|
+
mode,
|
|
221
|
+
value: typeof raw === "string" ? raw : JSON.stringify(raw ?? ""),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** `action` then every `extra_actions` entry, in order (schema v8). Mirrors
|
|
225
|
+
* the engine's `clause_actions`; never read `.action` alone. */
|
|
226
|
+
export function clauseActions(clause: unknown): DerivationAction[] {
|
|
227
|
+
const out: DerivationAction[] = [];
|
|
228
|
+
const first = readObject(clause, "action");
|
|
229
|
+
if (first) out.push(parseAction(first));
|
|
230
|
+
for (const extra of readArray(clause, "extra_actions")) {
|
|
231
|
+
if (extra && typeof extra === "object") out.push(parseAction(extra));
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
export function derivationRules(inner: unknown): DerivationRule[] {
|
|
236
|
+
return readArray(inner, "rules").map((rule) => {
|
|
237
|
+
const elseClause = readObject(rule, "else");
|
|
238
|
+
return {
|
|
239
|
+
branches: readArray(rule, "branches").map((b) => ({
|
|
240
|
+
condition: parseCondition(readObject(b, "condition")),
|
|
241
|
+
actions: clauseActions(b),
|
|
242
|
+
})),
|
|
243
|
+
fallback: elseClause ? clauseActions(elseClause) : null,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export function conditionVars(c: Condition | null, out: string[] = []): string[] {
|
|
248
|
+
if (!c) return out;
|
|
249
|
+
if (c.kind === "test") out.push(c.var);
|
|
250
|
+
else for (const k of c.conditions) conditionVars(k, out);
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
export function hasGroups(c: Condition | null): boolean {
|
|
254
|
+
return c?.kind === "group";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─────────── variables in / out ───────────
|
|
258
|
+
|
|
259
|
+
export interface VarFlow {
|
|
260
|
+
/** Variables this payload sets (value writes). */
|
|
261
|
+
sets: string[];
|
|
262
|
+
/** Variables it reads. */
|
|
263
|
+
reads: string[];
|
|
264
|
+
/** Variables whose negatives it touches. */
|
|
265
|
+
negatives: string[];
|
|
266
|
+
}
|
|
267
|
+
function uniq(xs: string[]): string[] {
|
|
268
|
+
return [...new Set(xs.filter(Boolean))];
|
|
269
|
+
}
|
|
270
|
+
/** What one module sets / reads. Bundles fold their children. */
|
|
271
|
+
export function varFlow(payload: unknown): VarFlow {
|
|
272
|
+
const kind = payloadKind(payload);
|
|
273
|
+
if (kind === "bundle") {
|
|
274
|
+
const flows = readArray(payload, "children")
|
|
275
|
+
.filter((c) => (c as Obj | null)?.["enabled"] !== false)
|
|
276
|
+
.map(varFlow);
|
|
277
|
+
return {
|
|
278
|
+
sets: uniq(flows.flatMap((f) => f.sets)),
|
|
279
|
+
reads: uniq(flows.flatMap((f) => f.reads)),
|
|
280
|
+
negatives: uniq(flows.flatMap((f) => f.negatives)),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const inner = innerOf(payload);
|
|
284
|
+
const sets: string[] = [];
|
|
285
|
+
const reads: string[] = [];
|
|
286
|
+
const negatives: string[] = [];
|
|
287
|
+
const readText = (t: string): void => {
|
|
288
|
+
const s = scanText(t);
|
|
289
|
+
reads.push(...s.vars, ...s.negVars);
|
|
290
|
+
};
|
|
291
|
+
if (kind === "wildcard") {
|
|
292
|
+
const b = readString(inner, "var_binding");
|
|
293
|
+
if (b) sets.push(b);
|
|
294
|
+
for (const o of wildcardOptions(inner)) {
|
|
295
|
+
readText(o.value);
|
|
296
|
+
if (o.negative) {
|
|
297
|
+
readText(o.negative);
|
|
298
|
+
if (b) negatives.push(b);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} else if (kind === "fixed_values") {
|
|
302
|
+
for (const v of fixedValues(inner)) {
|
|
303
|
+
sets.push(v.name);
|
|
304
|
+
readText(v.value);
|
|
305
|
+
if (v.negative) negatives.push(v.name);
|
|
306
|
+
}
|
|
307
|
+
} else if (kind === "combine") {
|
|
308
|
+
const out = readString(inner, "output_var");
|
|
309
|
+
if (out) sets.push(out);
|
|
310
|
+
reads.push(...readStrings(inner, "input_vars"));
|
|
311
|
+
readText(readString(inner, "template") ?? "");
|
|
312
|
+
const neg = readString(inner, "negative") ?? "";
|
|
313
|
+
if (neg) {
|
|
314
|
+
readText(neg);
|
|
315
|
+
if (out) negatives.push(out);
|
|
316
|
+
}
|
|
317
|
+
} else if (kind === "derivation") {
|
|
318
|
+
for (const r of derivationRules(inner)) {
|
|
319
|
+
const clauses = [...r.branches.map((b) => b.actions), ...(r.fallback ? [r.fallback] : [])];
|
|
320
|
+
for (const b of r.branches) reads.push(...conditionVars(b.condition));
|
|
321
|
+
for (const a of clauses.flat()) {
|
|
322
|
+
readText(a.value);
|
|
323
|
+
if (a.op === "neg_add" || a.op === "neg_replace") negatives.push(a.target);
|
|
324
|
+
else sets.push(a.target);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { sets: uniq(sets), reads: uniq(reads), negatives: uniq(negatives) };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─────────── feature pills ───────────
|
|
332
|
+
|
|
333
|
+
/** Short labels for the notable features a payload uses, shown as pills in
|
|
334
|
+
* the post header. Order is stable (most notable first). */
|
|
335
|
+
export function payloadFeatures(payload: unknown): string[] {
|
|
336
|
+
const kind = payloadKind(payload);
|
|
337
|
+
const found = new Set<string>();
|
|
338
|
+
const addText = (t: string): void => {
|
|
339
|
+
const s = scanText(t);
|
|
340
|
+
if (s.refs) found.add("@refs");
|
|
341
|
+
if (s.picks) found.add("picks");
|
|
342
|
+
if (s.negVars.length) found.add("$var.neg");
|
|
343
|
+
};
|
|
344
|
+
const visit = (p: unknown): void => {
|
|
345
|
+
const k = payloadKind(p);
|
|
346
|
+
const inner = innerOf(p);
|
|
347
|
+
if (k === "wildcard") {
|
|
348
|
+
if (readGroups(inner, "tag_groups")) found.add("tag axes");
|
|
349
|
+
for (const o of wildcardOptions(inner)) {
|
|
350
|
+
addText(o.value);
|
|
351
|
+
if (o.weight !== 1) found.add("weights");
|
|
352
|
+
if (o.negative) {
|
|
353
|
+
found.add("negatives");
|
|
354
|
+
addText(o.negative);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else if (k === "fixed_values") {
|
|
358
|
+
for (const v of fixedValues(inner)) {
|
|
359
|
+
addText(v.value);
|
|
360
|
+
if (v.negative) found.add("negatives");
|
|
361
|
+
}
|
|
362
|
+
} else if (k === "combine") {
|
|
363
|
+
addText(readString(inner, "template") ?? "");
|
|
364
|
+
const neg = readString(inner, "negative");
|
|
365
|
+
if (neg) {
|
|
366
|
+
found.add("negatives");
|
|
367
|
+
addText(neg);
|
|
368
|
+
}
|
|
369
|
+
} else if (k === "derivation") {
|
|
370
|
+
for (const r of derivationRules(inner)) {
|
|
371
|
+
const clauses = [...r.branches.map((b) => b.actions), ...(r.fallback ? [r.fallback] : [])];
|
|
372
|
+
if (r.branches.some((b) => hasGroups(b.condition))) found.add("AND / OR");
|
|
373
|
+
if (clauses.some((c) => c.length > 1)) found.add("multi-action");
|
|
374
|
+
for (const a of clauses.flat()) {
|
|
375
|
+
addText(a.value);
|
|
376
|
+
if (a.op === "neg_add" || a.op === "neg_replace") found.add("negatives");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else if (k === "constraint") {
|
|
380
|
+
const matrix = readObject(inner, "matrix") ?? {};
|
|
381
|
+
const onlyUsed =
|
|
382
|
+
Object.values(matrix).some((row) =>
|
|
383
|
+
Object.values((row as Obj) ?? {}).some((c) => readString(c, "mode") === "only"),
|
|
384
|
+
) || readArray(inner, "exceptions").some((e) => readString(e, "mode") === "only");
|
|
385
|
+
if (onlyUsed) found.add("only rule");
|
|
386
|
+
const sel = readObject(inner, "target_select");
|
|
387
|
+
const mode = readString(sel, "mode");
|
|
388
|
+
if (mode && mode !== "all") {
|
|
389
|
+
const count = readNumber(sel, "count");
|
|
390
|
+
found.add(`reach: ${mode}${count !== null ? ` ${count}` : ""}`);
|
|
391
|
+
}
|
|
392
|
+
} else if (k === "bundle") {
|
|
393
|
+
for (const c of readArray(p, "children")) visit(c);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
visit(payload);
|
|
397
|
+
const order = ["tag axes", "weights", "negatives", "$var.neg", "AND / OR", "multi-action", "only rule", "@refs", "picks"];
|
|
398
|
+
const sorted = order.filter((f) => found.has(f));
|
|
399
|
+
for (const f of found) if (!order.includes(f)) sorted.push(f);
|
|
400
|
+
return kind ? sorted : [];
|
|
401
|
+
}
|
package/src/preview.css
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* Shared chrome for the per-type payload previews (stats strip, toolbar,
|
|
2
|
+
row tables, section labels). Each preview pulls it in scoped
|
|
3
|
+
(`<style scoped src="./preview.css">`), so it styles only that piece's own
|
|
4
|
+
markup. Every selector is `.wp-pv-*`. */
|
|
5
|
+
|
|
6
|
+
.wp-pv {
|
|
7
|
+
container-type: inline-size;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* ── stats strip ─────────────────────────────────────────────── */
|
|
12
|
+
.wp-pv-stats {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
border-bottom: 1px solid var(--wp-border);
|
|
16
|
+
}
|
|
17
|
+
.wp-pv-stat {
|
|
18
|
+
padding: 12px 18px;
|
|
19
|
+
border-right: 1px solid var(--wp-border);
|
|
20
|
+
min-width: 0;
|
|
21
|
+
}
|
|
22
|
+
.wp-pv-stat b {
|
|
23
|
+
display: block;
|
|
24
|
+
font-size: 17px;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
color: var(--wp-text);
|
|
27
|
+
font-variant-numeric: tabular-nums;
|
|
28
|
+
white-space: nowrap;
|
|
29
|
+
}
|
|
30
|
+
.wp-pv-stat span {
|
|
31
|
+
font-size: 11.5px;
|
|
32
|
+
color: var(--wp-text-muted);
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ── toolbar ─────────────────────────────────────────────────── */
|
|
37
|
+
.wp-pv-tool {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: 8px 10px;
|
|
42
|
+
padding: 12px 16px;
|
|
43
|
+
border-bottom: 1px solid var(--wp-border);
|
|
44
|
+
}
|
|
45
|
+
.wp-pv-search {
|
|
46
|
+
width: 220px;
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
}
|
|
49
|
+
.wp-pv-label {
|
|
50
|
+
font-size: 10.5px;
|
|
51
|
+
color: var(--wp-text-dim);
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
letter-spacing: 0.05em;
|
|
54
|
+
}
|
|
55
|
+
.wp-pv-tool__end {
|
|
56
|
+
margin-left: auto;
|
|
57
|
+
}
|
|
58
|
+
.wp-pv-count {
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
color: var(--wp-text-muted);
|
|
61
|
+
font-variant-numeric: tabular-nums;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── row tables ──────────────────────────────────────────────── */
|
|
65
|
+
.wp-pv-table {
|
|
66
|
+
width: 100%;
|
|
67
|
+
border-collapse: collapse;
|
|
68
|
+
table-layout: fixed;
|
|
69
|
+
}
|
|
70
|
+
.wp-pv-table th,
|
|
71
|
+
.wp-pv-table td {
|
|
72
|
+
padding: 7px 12px;
|
|
73
|
+
border-bottom: 1px solid var(--wp-border);
|
|
74
|
+
text-align: left;
|
|
75
|
+
vertical-align: top;
|
|
76
|
+
}
|
|
77
|
+
.wp-pv-table th {
|
|
78
|
+
font-size: 10.5px;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
color: var(--wp-text-dim);
|
|
81
|
+
text-transform: uppercase;
|
|
82
|
+
letter-spacing: 0.05em;
|
|
83
|
+
padding-top: 8px;
|
|
84
|
+
padding-bottom: 6px;
|
|
85
|
+
}
|
|
86
|
+
.wp-pv-table td {
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
color: var(--wp-text);
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
overflow-wrap: anywhere;
|
|
91
|
+
}
|
|
92
|
+
.wp-pv-col-n {
|
|
93
|
+
width: 40px;
|
|
94
|
+
}
|
|
95
|
+
.wp-pv-table .wp-pv-n {
|
|
96
|
+
width: 40px;
|
|
97
|
+
text-align: right;
|
|
98
|
+
color: var(--wp-text-dim);
|
|
99
|
+
font-family: var(--wp-font-mono);
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
}
|
|
102
|
+
.wp-pv-table .wp-pv-var {
|
|
103
|
+
font-family: var(--wp-font-mono);
|
|
104
|
+
font-size: 12px;
|
|
105
|
+
color: var(--wp-success);
|
|
106
|
+
}
|
|
107
|
+
.wp-pv-neg-cell {
|
|
108
|
+
color: var(--wp-danger-text, var(--wp-danger));
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
}
|
|
111
|
+
.wp-pv-clamp {
|
|
112
|
+
display: -webkit-box;
|
|
113
|
+
-webkit-line-clamp: 2;
|
|
114
|
+
-webkit-box-orient: vertical;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
.wp-pv-more {
|
|
118
|
+
padding: 4px 12px;
|
|
119
|
+
border-bottom: 1px solid var(--wp-border);
|
|
120
|
+
}
|
|
121
|
+
.wp-pv-empty {
|
|
122
|
+
margin: 0;
|
|
123
|
+
padding: 18px 16px;
|
|
124
|
+
color: var(--wp-text-muted);
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Wildcard fallback option (schema v9): rolls normally, also stands in. */
|
|
129
|
+
.wp-pv-fbtag {
|
|
130
|
+
font-family: var(--wp-font-mono);
|
|
131
|
+
font-size: 10px;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
letter-spacing: 0.04em;
|
|
134
|
+
padding: 1px 6px;
|
|
135
|
+
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
136
|
+
background: color-mix(in oklab, var(--wp-accent-500) 18%, transparent);
|
|
137
|
+
color: var(--wp-accent-text);
|
|
138
|
+
white-space: nowrap;
|
|
139
|
+
}
|
|
140
|
+
/* NEG tag, shared by every negative cell / block. */
|
|
141
|
+
.wp-pv-negtag {
|
|
142
|
+
font-family: var(--wp-font-mono);
|
|
143
|
+
font-size: 9px;
|
|
144
|
+
font-weight: 700;
|
|
145
|
+
letter-spacing: 0.06em;
|
|
146
|
+
padding: 0 4px;
|
|
147
|
+
margin-right: 5px;
|
|
148
|
+
border-radius: min(3px, var(--wp-radius-cap, 999px));
|
|
149
|
+
background: color-mix(in oklab, var(--wp-danger) 18%, transparent);
|
|
150
|
+
color: var(--wp-danger-text, var(--wp-danger));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── section label + text block ──────────────────────────────── */
|
|
154
|
+
.wp-pv-sec {
|
|
155
|
+
padding: 14px 16px 6px;
|
|
156
|
+
font-size: 10.5px;
|
|
157
|
+
color: var(--wp-text-dim);
|
|
158
|
+
text-transform: uppercase;
|
|
159
|
+
letter-spacing: 0.05em;
|
|
160
|
+
}
|
|
161
|
+
.wp-pv-block {
|
|
162
|
+
margin: 0 16px 4px;
|
|
163
|
+
padding: 12px 14px;
|
|
164
|
+
background: var(--wp-bg-2);
|
|
165
|
+
border: 1px solid var(--wp-border);
|
|
166
|
+
border-radius: min(10px, var(--wp-radius-cap, 999px));
|
|
167
|
+
font-family: var(--wp-font-mono);
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
line-height: 1.9;
|
|
170
|
+
color: var(--wp-text);
|
|
171
|
+
overflow-wrap: anywhere;
|
|
172
|
+
}
|
|
173
|
+
.wp-pv-block--neg {
|
|
174
|
+
border-color: color-mix(in oklab, var(--wp-danger) 28%, transparent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Narrow hosts (embed, phones): drop secondary columns. */
|
|
178
|
+
@container (max-width: 640px) {
|
|
179
|
+
.wp-pv-hide-narrow {
|
|
180
|
+
display: none;
|
|
181
|
+
}
|
|
182
|
+
.wp-pv-stat {
|
|
183
|
+
padding: 10px 12px;
|
|
184
|
+
}
|
|
185
|
+
.wp-pv-table td,
|
|
186
|
+
.wp-pv-table th {
|
|
187
|
+
padding-left: 8px;
|
|
188
|
+
padding-right: 8px;
|
|
189
|
+
}
|
|
190
|
+
.wp-pv-col-n,
|
|
191
|
+
.wp-pv-table .wp-pv-n {
|
|
192
|
+
width: 28px;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/slider-math.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Number helpers for
|
|
2
|
+
* Number helpers for Slider (one thumb or a pair): clamp
|
|
3
3
|
* to the ends, snap to the step counted from `min`, and turn a pointer
|
|
4
4
|
* position into a value. Steps like 0.1 snap without float fuzz.
|
|
5
5
|
*/
|
|
@@ -43,3 +43,47 @@ export function keyStep(key: string, v: number, min: number, max: number, step:
|
|
|
43
43
|
default: return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mark snapping. `snap: true` keeps the value on a mark (the nearest one);
|
|
49
|
+
* a number pulls it onto a mark only within that distance, so the thumb
|
|
50
|
+
* moves freely by its step elsewhere and "clicks" into a mark near one.
|
|
51
|
+
* With no marks, or snap off, the value comes back as it was.
|
|
52
|
+
*/
|
|
53
|
+
export function snapToMarks(v: number, marks: readonly number[], snapTo: boolean | number | undefined): number {
|
|
54
|
+
if (!snapTo || !marks.length) return v;
|
|
55
|
+
let best = marks[0] ?? v;
|
|
56
|
+
for (const m of marks) if (Math.abs(m - v) < Math.abs(best - v)) best = m;
|
|
57
|
+
if (snapTo === true) return best;
|
|
58
|
+
return Math.abs(best - v) <= snapTo ? best : v;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A key press on a slider that only takes marks (`snap: true`): arrows go to
|
|
63
|
+
* the next mark up or down, PageUp/PageDown jump `big` marks, Home/End the
|
|
64
|
+
* first and last. Null for a key the slider ignores.
|
|
65
|
+
*/
|
|
66
|
+
export function keyToMark(key: string, v: number, marks: readonly number[], big = 3): number | null {
|
|
67
|
+
const sorted = [...new Set(marks)].sort((a, b) => a - b);
|
|
68
|
+
if (!sorted.length) return null;
|
|
69
|
+
// Where v sits: the index of the mark at it, or between two marks.
|
|
70
|
+
const at = sorted.findIndex((m) => m >= v);
|
|
71
|
+
const exact = at >= 0 && sorted[at] === v;
|
|
72
|
+
const up = (n: number): number => {
|
|
73
|
+
const from = at < 0 ? sorted.length : exact ? at : at - 1;
|
|
74
|
+
return sorted[Math.min(sorted.length - 1, from + n)] ?? v;
|
|
75
|
+
};
|
|
76
|
+
const down = (n: number): number => {
|
|
77
|
+
const from = at < 0 ? sorted.length : at;
|
|
78
|
+
return sorted[Math.max(0, from - n)] ?? v;
|
|
79
|
+
};
|
|
80
|
+
switch (key) {
|
|
81
|
+
case "ArrowRight": case "ArrowUp": return up(1);
|
|
82
|
+
case "ArrowLeft": case "ArrowDown": return down(1);
|
|
83
|
+
case "PageUp": return up(big);
|
|
84
|
+
case "PageDown": return down(big);
|
|
85
|
+
case "Home": return sorted[0] ?? v;
|
|
86
|
+
case "End": return sorted[sorted.length - 1] ?? v;
|
|
87
|
+
default: return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/theme/looks.css
CHANGED
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
/* --wp-radius-cap: the roundest a corner may get in this look. Fixed radii
|
|
48
48
|
in components are written min(Npx, var(--wp-radius-cap, 999px)) and
|
|
49
49
|
pills var(--wp-radius-cap, 999px), so Orbit draws them as authored and a
|
|
50
|
-
square look squares them without a rule per component. Circles
|
|
51
|
-
|
|
50
|
+
square look squares them without a rule per component. Circles are
|
|
51
|
+
min(50%, var(--wp-radius-cap, 999px)): round in Orbit, softened in Crisp,
|
|
52
|
+
square in Terminal and TUI (dots, radios, avatars, the timeline).
|
|
53
|
+
src/__tests__/corners.test.ts fails on a raw radius. The theme audit (scripts/theme-audit) fails on a corner
|
|
52
54
|
rounder than the cap. */
|
|
53
55
|
html[data-wp-look="crisp"] { --wp-radius-cap: 4px; }
|
|
54
56
|
html[data-wp-look="terminal"],
|
|
@@ -362,10 +364,8 @@ html[data-wp-look="tui"] .wp-check:is([aria-checked="true"], [data-checked="true
|
|
|
362
364
|
html[data-wp-look="tui"] .wp-check:is([aria-checked="mixed"], [data-indeterminate="true"]):not([data-checked="true"])::before { content: "[-]"; color: var(--wp-accent-text); }
|
|
363
365
|
html[data-wp-look="tui"] .wp-page__title { font-size: 16px; font-weight: 700; letter-spacing: 0; }
|
|
364
366
|
/* Radios read ( ) / (*) beside the [ ] / [x] checkboxes; the drawn dot
|
|
365
|
-
hides. RadioGroup marks the picked option on the row (data-checked)
|
|
366
|
-
|
|
367
|
-
html[data-wp-look="tui"] .wp-radio-group__opt .wp-radio-group__dot,
|
|
368
|
-
html[data-wp-look="tui"] .wp-radio {
|
|
367
|
+
hides. RadioGroup marks the picked option on the row (data-checked). */
|
|
368
|
+
html[data-wp-look="tui"] .wp-radio-group__opt .wp-radio-group__dot {
|
|
369
369
|
width: auto;
|
|
370
370
|
min-width: 3ch;
|
|
371
371
|
height: auto;
|
|
@@ -381,19 +381,6 @@ html[data-wp-look="tui"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot
|
|
|
381
381
|
html[data-wp-look="tui"] .wp-radio-group__dot::before { content: "( )"; }
|
|
382
382
|
html[data-wp-look="tui"] .wp-radio-group__opt[data-checked] .wp-radio-group__dot::before { content: "(*)"; color: var(--wp-accent-text); }
|
|
383
383
|
html[data-wp-look="tui"] .wp-radio-group__opt[aria-disabled="true"][data-checked] .wp-radio-group__dot::before { color: var(--wp-text-dim); }
|
|
384
|
-
html[data-wp-look="tui"] .wp-radio::before,
|
|
385
|
-
html[data-wp-look="tui"] .wp-radio:disabled::before {
|
|
386
|
-
content: "( )";
|
|
387
|
-
width: auto;
|
|
388
|
-
height: auto;
|
|
389
|
-
border-radius: 0;
|
|
390
|
-
background: transparent;
|
|
391
|
-
transform: none;
|
|
392
|
-
}
|
|
393
|
-
html[data-wp-look="tui"] .wp-radio:checked::before { content: "(*)"; color: var(--wp-accent-text); }
|
|
394
|
-
html[data-wp-look="tui"] .wp-radio:disabled:checked::before { content: "(*)"; color: var(--wp-text-dim); }
|
|
395
|
-
html[data-wp-look="tui"] .wp-radio:checked,
|
|
396
|
-
html[data-wp-look="tui"] .wp-radio:disabled:checked { background: transparent; }
|
|
397
384
|
|
|
398
385
|
/* ================= Canvas previews ignore the look =================
|
|
399
386
|
The canvas keeps ComfyUI's ground in every look (only the accent follows
|