@dumiflex/wp-kit 0.12.0 → 0.14.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0](https://github.com/DumiFlex/wp-kit/compare/v0.13.0...v0.14.0) (2026-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * canvas controls at ComfyUI's own sizes ([#105](https://github.com/DumiFlex/wp-kit/issues/105)) ([22e6d62](https://github.com/DumiFlex/wp-kit/commit/22e6d62834d197ef4c4ce64a463fbbbe50f75610))
9
+
10
+ ## [0.13.0](https://github.com/DumiFlex/wp-kit/compare/v0.12.0...v0.13.0) (2026-11-03)
11
+
12
+
13
+ ### Features
14
+
15
+ * v-toggle-row, so a row's words flip its one checkbox or switch, and SettingRow rows do it ([#102](https://github.com/DumiFlex/wp-kit/issues/102)) ([1d3309b](https://github.com/DumiFlex/wp-kit/commit/1d3309b6f2a7da6f2dff82a41aed8bc46d1c779a))
16
+
3
17
  ## [0.12.0](https://github.com/DumiFlex/wp-kit/compare/v0.11.0...v0.12.0) (2026-11-03)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dumiflex/wp-kit",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Vue 3 component kit shared by the Wildcard Pipeline manager, canvas and community site.",
5
5
  "keywords": [
6
6
  "vue",
package/src/Checkbox.vue CHANGED
@@ -8,8 +8,9 @@
8
8
  * <Checkbox v-model="favorites" field label="Favorites only" />
9
9
  *
10
10
  * Box: a `<button role="checkbox">`, so a wrapping `<label>` ticks it like an
11
- * input. Click, Space and Enter toggle. `size="sm"` is the 12px box of the
12
- * canvas rows' on/off mark; md is 14px. A parent can tint the checked fill
11
+ * input. Click, Space and Enter toggle. `size="sm"` is the 14px box of the
12
+ * canvas rows' on/off mark; md is 16px (ComfyUI's own box). Either takes a
13
+ * 24px click around it, so a lone box is as easy to hit as a native row. A parent can tint the checked fill
13
14
  * with `--check-on` (a bundle header uses its bundle colour).
14
15
  *
15
16
  * Without a label the box is the root: `class`, `title`, `data-test` and a
@@ -170,8 +171,9 @@ function toggle(): void {
170
171
  .wp-check {
171
172
  appearance: none;
172
173
  box-sizing: border-box;
173
- width: 14px;
174
- height: 14px;
174
+ position: relative;
175
+ width: 16px;
176
+ height: 16px;
175
177
  flex-shrink: 0;
176
178
  display: inline-flex;
177
179
  align-items: center;
@@ -188,9 +190,12 @@ function toggle(): void {
188
190
  cursor: pointer;
189
191
  transition: background var(--wp-motion-quick), border-color var(--wp-motion-quick), color var(--wp-motion-quick);
190
192
  }
191
- .wp-check svg { width: 9px; height: 9px; display: block; pointer-events: none; }
192
- .wp-check--sm { width: 12px; height: 12px; border-width: 1px; border-radius: min(2px, var(--wp-radius-cap, 999px)); }
193
- .wp-check--sm svg { width: 7px; height: 7px; }
193
+ .wp-check svg { width: 10px; height: 10px; display: block; pointer-events: none; }
194
+ /* The click reaches 24 x 24 around the box (::before is the TUI look's [x]). */
195
+ .wp-check::after { content: ""; position: absolute; inset: -4px; }
196
+ .wp-check--sm { width: 14px; height: 14px; border-width: 1px; border-radius: min(2px, var(--wp-radius-cap, 999px)); }
197
+ .wp-check--sm::after { inset: -5px; }
198
+ .wp-check--sm svg { width: 8px; height: 8px; }
194
199
  .wp-check:focus-visible { outline: none; box-shadow: var(--wp-focus-ring); }
195
200
  .wp-check:where(:not(:disabled, [aria-checked="true"], [aria-checked="mixed"]):hover) { border-color: var(--wp-accent-500); }
196
201
  .wp-check:where([aria-invalid="true"]) { border-color: var(--wp-error-border); }
@@ -14,7 +14,13 @@
14
14
  * control column, like a form. `recommended` names the value most people
15
15
  * should pick ("On"). `for` makes the label a real `<label for>`. Slots:
16
16
  * `#badge` after the label, `#extra` under the hint (a status line).
17
+ *
18
+ * A row whose control is one checkbox or switch flips it on a click anywhere
19
+ * on the row's words (v-toggle-row), like a label; a WpSwitch is a span, so a
20
+ * `<label for>` alone can't reach it.
17
21
  */
22
+ import { vToggleRow } from "./toggle-row";
23
+
18
24
  const props = defineProps<{
19
25
  label: string;
20
26
  hint?: string | undefined;
@@ -32,6 +38,7 @@ const props = defineProps<{
32
38
  <template>
33
39
  <div
34
40
  :id="settingKey"
41
+ v-toggle-row
35
42
  class="wp-set-row"
36
43
  :class="{ 'wp-set-row--stacked': stacked, 'wp-set-row--form': form, 'wp-set-row--target': highlighted }"
37
44
  :data-setting="settingKey"
@@ -58,6 +58,7 @@ const emit = defineEmits<{ click: [evt: MouseEvent] }>();
58
58
  display: inline-flex;
59
59
  align-items: center;
60
60
  justify-content: center;
61
+ position: relative;
61
62
  flex: none;
62
63
  box-sizing: border-box;
63
64
  width: 20px;
@@ -72,6 +73,9 @@ const emit = defineEmits<{ click: [evt: MouseEvent] }>();
72
73
  cursor: pointer;
73
74
  transition: background var(--wp-motion-quick), border-color var(--wp-motion-quick), color var(--wp-motion-quick);
74
75
  }
76
+ /* The drawn box stays 20 px (it lines up with ComfyUI's own icon-sm and our
77
+ rows); the click reaches 24 x 24, ComfyUI's row control size. */
78
+ .wp-icon-btn::after { content: ""; position: absolute; inset: -3px; }
75
79
  .wp-icon-btn[data-tone="accent"] { --tone: var(--wp-accent); }
76
80
  .wp-icon-btn[data-tone="warn"] { --tone: var(--wp-warn); }
77
81
  .wp-icon-btn[data-tone="danger"] { --tone: var(--wp-danger); }
package/src/WpNumber.vue CHANGED
@@ -142,13 +142,15 @@ function bump(dir: 1 | -1): void {
142
142
  <style scoped src="./field.css"></style>
143
143
 
144
144
  <style scoped>
145
+ /* Spinner columns sized so each arrow is a real target in the box
146
+ * (sm: 20 x 12 in a 24 px field, near ComfyUI's 24 px -/+ buttons). */
145
147
  .wp-cnum {
146
- --cnum-spin: 16px;
148
+ --cnum-spin: 20px;
147
149
  display: inline-flex; align-items: stretch; height: var(--wp-field-h); min-width: 0; overflow: hidden;
148
150
  font-family: var(--wp-font-mono);
149
151
  }
150
- .wp-cnum--md { --cnum-spin: 20px; }
151
- .wp-cnum--lg { --cnum-spin: 22px; }
152
+ .wp-cnum--md { --cnum-spin: 24px; }
153
+ .wp-cnum--lg { --cnum-spin: 28px; }
152
154
  .wp-cnum__field {
153
155
  flex: 1; min-width: 0; padding: 0 6px 0 8px; background: transparent; border: 0;
154
156
  color: var(--wp-text); font: inherit; text-align: right;
@@ -170,8 +172,8 @@ function bump(dir: 1 | -1): void {
170
172
  flex: 1; display: flex; align-items: center; justify-content: center; padding: 0; border: 0;
171
173
  background: transparent; color: var(--wp-text-dim); cursor: pointer; line-height: 0;
172
174
  }
173
- .wp-cnum__btn .pi { font-size: 7px; }
174
- .wp-cnum--md .wp-cnum__btn .pi, .wp-cnum--lg .wp-cnum__btn .pi { font-size: 8px; }
175
+ .wp-cnum__btn .pi { font-size: 10px; }
176
+ .wp-cnum--md .wp-cnum__btn .pi, .wp-cnum--lg .wp-cnum__btn .pi { font-size: 10px; }
175
177
  .wp-cnum__btn + .wp-cnum__btn { border-top: 1px solid var(--wp-border); }
176
178
  .wp-cnum__btn:hover:not(:disabled) { color: var(--wp-accent-text); background: color-mix(in oklab, var(--wp-accent) 12%, transparent); }
177
179
  .wp-cnum__btn:disabled { cursor: not-allowed; }
@@ -127,7 +127,9 @@ function onKeydown(ev: KeyboardEvent): void {
127
127
  height: var(--wp-control-h-md);
128
128
  box-sizing: border-box;
129
129
  }
130
- .wp-segmented[data-size="sm"] { height: var(--wp-control-h-sm); border-radius: var(--wp-radius-sm); }
130
+ /* sm: a 1 px inset, so each option is 20 px tall in the 24 px box (a bigger
131
+ target in the same row, close to ComfyUI's 24 px toggle-group items). */
132
+ .wp-segmented[data-size="sm"] { height: var(--wp-control-h-sm); padding: 1px; border-radius: var(--wp-radius-sm); }
131
133
  .wp-segmented[data-size="lg"] { height: var(--wp-control-h-lg); }
132
134
  .wp-segmented__opt {
133
135
  display: inline-flex;
package/src/WpSwitch.vue CHANGED
@@ -64,12 +64,13 @@ function flip(): void {
64
64
  <style scoped>
65
65
  /* Two looks. track: a sliding knob, for a setting that takes effect at once.
66
66
  * chip: a labelled chip that lights up when on (Hide from prompt, Lock...).
67
- * Heights follow the control scale (sm 24 / md 32 / lg 40). */
67
+ * Heights follow the control scale (sm 24 / md 32 / lg 40). The sm track is
68
+ * ComfyUI's own Nodes 2.0 switch (36 x 20, 16 px knob, 24 px row). */
68
69
  .wp-switch {
69
70
  display: inline-flex;
70
71
  align-items: center;
71
72
  gap: var(--wp-space-3);
72
- min-height: 20px;
73
+ min-height: 24px;
73
74
  flex-shrink: 0;
74
75
  color: var(--wp-text-muted);
75
76
  font: var(--wp-text-xs) / 1 var(--wp-font-sans);
@@ -80,12 +81,12 @@ function flip(): void {
80
81
  }
81
82
  .wp-switch[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
82
83
  .wp-switch__label { color: var(--wp-text-muted); }
83
- .wp-switch--md, .wp-switch--lg { min-height: 24px; font-size: var(--wp-text-sm); }
84
+ .wp-switch--md, .wp-switch--lg { min-height: 32px; font-size: var(--wp-text-sm); }
84
85
 
85
86
  /* Track look */
86
87
  .wp-switch__track {
87
- --switch-w: 26px;
88
- --switch-h: 14px;
88
+ --switch-w: 36px;
89
+ --switch-h: 20px;
89
90
  position: relative;
90
91
  flex-shrink: 0;
91
92
  width: var(--switch-w);
@@ -98,8 +99,8 @@ function flip(): void {
98
99
  background: var(--wp-bg-3);
99
100
  transition: background var(--wp-motion-quick), border-color var(--wp-motion-quick);
100
101
  }
101
- .wp-switch--md .wp-switch__track { --switch-w: 32px; --switch-h: 18px; }
102
- .wp-switch--lg .wp-switch__track { --switch-w: 40px; --switch-h: 22px; }
102
+ .wp-switch--md .wp-switch__track { --switch-w: 40px; --switch-h: 22px; }
103
+ .wp-switch--lg .wp-switch__track { --switch-w: 48px; --switch-h: 26px; }
103
104
  .wp-switch__knob {
104
105
  position: absolute;
105
106
  top: 1px;
@@ -0,0 +1,7 @@
1
+ /* v-toggle-row (toggle-row.ts): the row's words flip its one toggle, so the
2
+ * whole row shows the hand and its words don't get picked up as a selection
3
+ * on a quick double click. Layered, so a host's own cursor rule wins. */
4
+ @layer wp-extension {
5
+ .wp-toggle-row { cursor: pointer; user-select: none; -webkit-user-select: none; }
6
+ .wp-toggle-row--disabled { cursor: not-allowed; }
7
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `v-toggle-row`: a click on a row's words flips the row's one checkbox or
3
+ * switch, the way a `<label>` ticks its input. For a row that lays out its
4
+ * label and its control itself (a label on the left, a switch on the right)
5
+ * instead of passing the words to the Checkbox or WpSwitch.
6
+ *
7
+ * <div v-toggle-row class="row">
8
+ * <span>Override base seed</span>
9
+ * <WpSwitch v-model="override" aria-label="Override base seed" />
10
+ * </div>
11
+ *
12
+ * It acts only when the row holds exactly one `role="checkbox"` or
13
+ * `role="switch"` and no other value control (an input, a select, a slider,
14
+ * radios), so a row with two switches or a switch beside a number field stays
15
+ * as it is. A click on anything interactive inside the row (the toggle
16
+ * itself, a button, a link, a native `<label>`, a labelled Checkbox) is left
17
+ * to that element. The control keeps the keyboard stop and gets the focus.
18
+ * SettingRow uses it on every row.
19
+ *
20
+ * The row gets the `wp-toggle-row` class while it has a single toggle, for
21
+ * the pointer cursor (toggle-row.css). State lives in a WeakMap (no expando
22
+ * keys), so the row's own attributes and serialization stay untouched.
23
+ */
24
+ import type { Directive } from "vue";
25
+ import "./toggle-row.css";
26
+
27
+ const TOGGLE = '[role="checkbox"], [role="switch"]';
28
+ /** Value controls that make "the row's control" ambiguous. */
29
+ const OTHER_CONTROL =
30
+ 'input:not([type="hidden"]), select, textarea, [role="combobox"], [role="slider"], [role="spinbutton"], [role="radio"], [role="listbox"], [contenteditable=""], [contenteditable="true"]';
31
+ /** A click that lands on one of these belongs to it, not to the row. */
32
+ const INTERACTIVE =
33
+ 'button, a[href], input, select, textarea, label, summary, [role="button"], [role="link"], [role="checkbox"], [role="switch"], [role="radio"], [role="tab"], [role="menuitem"], [role="option"], [role="combobox"], [role="slider"], [role="spinbutton"], [contenteditable=""], [contenteditable="true"], .wp-checkbox';
34
+
35
+ interface State {
36
+ off: () => void;
37
+ }
38
+
39
+ const states = new WeakMap<HTMLElement, State>();
40
+ /** One click flips one toggle, even with rows nested in rows. */
41
+ const handled = new WeakSet<Event>();
42
+
43
+ /** The row's one toggle, or null when it has none, several, or another control beside it. */
44
+ export function rowToggle(row: HTMLElement): HTMLElement | null {
45
+ const toggles = row.querySelectorAll<HTMLElement>(TOGGLE);
46
+ if (toggles.length !== 1) return null;
47
+ if (row.querySelector(OTHER_CONTROL)) return null;
48
+ return toggles[0] ?? null;
49
+ }
50
+
51
+ function isDisabled(el: HTMLElement): boolean {
52
+ return el.getAttribute("aria-disabled") === "true" || (el instanceof HTMLButtonElement && el.disabled);
53
+ }
54
+
55
+ function sync(row: HTMLElement): void {
56
+ const toggle = rowToggle(row);
57
+ row.classList.toggle("wp-toggle-row", !!toggle);
58
+ row.classList.toggle("wp-toggle-row--disabled", !!toggle && isDisabled(toggle));
59
+ }
60
+
61
+ /** Flip the row's toggle for a click on the row's words. Exported for tests. */
62
+ export function onRowClick(row: HTMLElement, event: MouseEvent): void {
63
+ if (handled.has(event) || event.button !== 0) return;
64
+ const target = event.target;
65
+ if (!(target instanceof Element)) return;
66
+ const hit = target.closest(INTERACTIVE);
67
+ if (hit && hit !== row && row.contains(hit)) return;
68
+ // Text the reader was selecting is not a click on the label.
69
+ const selection = row.ownerDocument.getSelection?.();
70
+ if (selection && !selection.isCollapsed && row.contains(selection.anchorNode)) return;
71
+ const toggle = rowToggle(row);
72
+ if (!toggle || isDisabled(toggle)) return;
73
+ handled.add(event);
74
+ toggle.click();
75
+ toggle.focus({ preventScroll: true });
76
+ }
77
+
78
+ function attach(el: HTMLElement): void {
79
+ if (states.has(el)) return;
80
+ const click = (e: MouseEvent): void => onRowClick(el, e);
81
+ el.addEventListener("click", click);
82
+ states.set(el, { off: () => el.removeEventListener("click", click) });
83
+ }
84
+
85
+ function detach(el: HTMLElement): void {
86
+ states.get(el)?.off();
87
+ states.delete(el);
88
+ el.classList.remove("wp-toggle-row", "wp-toggle-row--disabled");
89
+ }
90
+
91
+ /** `v-toggle-row="false"` turns it off for one row (a row whose words do something else). */
92
+ export const vToggleRow: Directive<HTMLElement, boolean | undefined> = {
93
+ mounted(el, binding) {
94
+ if (binding.value === false) return;
95
+ attach(el);
96
+ sync(el);
97
+ },
98
+ updated(el, binding) {
99
+ if (binding.value === false) return detach(el);
100
+ attach(el);
101
+ sync(el);
102
+ },
103
+ unmounted: detach,
104
+ };