@dumiflex/wp-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +5 -0
  3. package/package.json +4 -3
  4. package/src/Avatar.vue +1 -1
  5. package/src/Banner.vue +4 -4
  6. package/src/BundlePreview.vue +254 -0
  7. package/src/Button.vue +73 -44
  8. package/src/Checkbox.vue +201 -63
  9. package/src/Chip.vue +5 -2
  10. package/src/CodeBlock.vue +1 -1
  11. package/src/ColorField.vue +60 -6
  12. package/src/CombinePreview.vue +150 -0
  13. package/src/Combobox.vue +10 -14
  14. package/src/ConfirmModal.vue +12 -22
  15. package/src/ConstraintMatrixPreview.vue +6 -22
  16. package/src/ConstraintPreview.vue +259 -0
  17. package/src/ContextMenu.vue +11 -30
  18. package/src/CopyButton.vue +1 -1
  19. package/src/DataTable.vue +0 -2
  20. package/src/DatePicker.vue +1 -1
  21. package/src/DerivationCondition.vue +68 -0
  22. package/src/DerivationPreview.vue +345 -0
  23. package/src/Disclosure.vue +2 -1
  24. package/src/Drawer.vue +2 -2
  25. package/src/DropZone.vue +2 -2
  26. package/src/FixedValuesPreview.vue +184 -0
  27. package/src/HsvPicker.vue +2 -2
  28. package/src/IconPicker.vue +32 -103
  29. package/src/IconPickerPanel.vue +132 -0
  30. package/src/ImageGrid.vue +2 -2
  31. package/src/Lightbox.vue +2 -2
  32. package/src/LoadingMark.vue +6 -61
  33. package/src/Modal.vue +109 -44
  34. package/src/ModalShell.vue +26 -5
  35. package/src/Pagination.vue +4 -4
  36. package/src/PayloadPreview.vue +154 -0
  37. package/src/Popover.vue +68 -12
  38. package/src/PostDetailSkeleton.vue +65 -0
  39. package/src/PostGridSkeleton.vue +61 -0
  40. package/src/PreviewGroup.vue +75 -0
  41. package/src/ProfileSkeleton.vue +73 -0
  42. package/src/RadioGroup.vue +5 -4
  43. package/src/RichText.vue +61 -0
  44. package/src/SearchInput.vue +1 -1
  45. package/src/Select.vue +53 -158
  46. package/src/SelectionBar.vue +90 -0
  47. package/src/SettingGroup.vue +57 -0
  48. package/src/SettingRow.vue +127 -0
  49. package/src/Skeleton.vue +1 -1
  50. package/src/Slider.vue +166 -55
  51. package/src/SortableList.vue +1 -1
  52. package/src/TagInput.vue +1 -1
  53. package/src/Timeline.vue +2 -2
  54. package/src/WildcardPreview.vue +607 -0
  55. package/src/WorkingLine.vue +234 -0
  56. package/src/WpIconButton.vue +3 -2
  57. package/src/WpImageZoom.vue +7 -7
  58. package/src/WpSegmented.vue +3 -1
  59. package/src/WpSwitch.vue +1 -1
  60. package/src/WpTabs.vue +1 -1
  61. package/src/_modal-motion.css +1 -1
  62. package/src/button.css +8 -6
  63. package/src/canvas-motion.ts +251 -0
  64. package/src/floating.ts +5 -0
  65. package/src/hsv.ts +1 -1
  66. package/src/kind-colour.ts +24 -1
  67. package/src/linkify.ts +142 -0
  68. package/src/loading-mark-motion.ts +12 -155
  69. package/src/matrix-axis.ts +28 -0
  70. package/src/menu.css +51 -0
  71. package/src/motion.css +73 -0
  72. package/src/pi-icons.ts +51 -0
  73. package/src/preview-readers.ts +401 -0
  74. package/src/preview.css +194 -0
  75. package/src/slider-math.ts +45 -1
  76. package/src/theme/looks.css +6 -19
  77. package/src/theme/tokens.css +12 -1108
  78. package/src/tokens.ts +15 -4
  79. package/src/tooltip.css +8 -5
  80. package/src/useDialogFocus.ts +11 -2
  81. package/src/working-line-motion.ts +135 -0
  82. package/src/CloseButton.vue +0 -69
  83. package/src/ColorPicker.vue +0 -201
  84. package/src/ConfirmDialog.vue +0 -165
  85. package/src/RangeSlider.vue +0 -183
  86. package/src/Toggle.vue +0 -36
  87. package/src/WpButton.vue +0 -75
  88. package/src/WpCheck.vue +0 -104
  89. package/src/_skeleton.css +0 -50
@@ -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
+ }
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Number helpers for sliders (Slider now, the two-thumb range later): clamp
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
+ }
@@ -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 (50%)
51
- stay circles. The theme audit (scripts/theme-audit) fails on a corner
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), the
366
- plain .wp-radio is a real input (:checked). */
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