inviton-powerduck 0.0.321 → 0.0.322

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.
@@ -1,474 +1,474 @@
1
- /* Styles for <DropdownNext>. Self-contained — the new dropdown emits no
2
- .select2-* class aliases and is independent of select2's stylesheet.
3
- The visual design (gray chips with × on the left, blue highlighted
4
- options, bold group headers, form-control 38px trigger) replicates
5
- select2's default theme but every rule below targets only .pd-dd-*
6
- classes our TSX emits. Verified visually against select2 via
7
- dev/validate-dropdown-parity.mjs. */
8
-
9
- .pd-dd-root {
10
- position: relative;
11
- display: inline-block;
12
- width: 100%;
13
- }
14
-
15
- /* ── Trigger (single & multi-chip, non-tags) ─────────────────────────── */
16
-
17
- /* Outer trigger shell — keeps the project's form-control look (Bootstrap-y,
18
- * #ced4da border, 38px min-height). Inner padding (8px left / 20px right)
19
- * matches select2's `.select2-selection__rendered`, so text and chips sit at
20
- * the same horizontal offset as in the legacy reference. */
21
- .pd-dd-trigger {
22
- display: flex;
23
- align-items: center;
24
- min-height: 38px;
25
- padding: 4px 20px 4px 8px;
26
- background: #fff;
27
- border: 1px solid #ced4da;
28
- border-radius: 4px;
29
- cursor: pointer;
30
- user-select: none;
31
- position: relative;
32
- box-sizing: border-box;
33
- font-size: 14px;
34
- color: #212529;
35
- line-height: 1.4;
36
- }
37
-
38
- .pd-dd-trigger.pd-dd-disabled,
39
- .pd-dd-trigger[aria-disabled="true"] {
40
- background: #e9ecef;
41
- cursor: not-allowed;
42
- color: #6c757d;
43
- }
44
-
45
- .pd-dd-trigger:focus-visible {
46
- outline: none;
47
- border-color: #3b82f6;
48
- }
49
-
50
- .pd-dd-root.pd-dd-open .pd-dd-trigger {
51
- border-bottom-left-radius: 0;
52
- border-bottom-right-radius: 0;
53
- }
54
-
55
- .pd-dd-trigger-text {
56
- flex: 1;
57
- min-width: 0;
58
- color: inherit;
59
- white-space: nowrap;
60
- overflow: hidden;
61
- text-overflow: ellipsis;
62
- }
63
-
64
- .pd-dd-trigger-text.pd-dd-placeholder {
65
- color: #999;
66
- }
67
-
68
- .pd-dd-trigger-arrow {
69
- position: absolute;
70
- right: 1px;
71
- top: 1px;
72
- bottom: 1px;
73
- width: 20px;
74
- display: flex;
75
- align-items: center;
76
- justify-content: center;
77
- pointer-events: none;
78
- }
79
-
80
- .pd-dd-trigger-arrow b {
81
- display: block;
82
- width: 0;
83
- height: 0;
84
- border: 5px solid transparent;
85
- border-top-color: #888;
86
- border-bottom: 0;
87
- }
88
-
89
- /* Single-select clear button (× shown when placeholder is set and a value
90
- * is selected; mirrors select2's allowClear behaviour). Sits on the right of
91
- * the trigger text, just before the arrow — matches select2's clear-button
92
- * placement. */
93
- .pd-dd-trigger-clear {
94
- display: inline-flex;
95
- align-items: center;
96
- justify-content: center;
97
- margin-left: 4px;
98
- color: #999;
99
- cursor: pointer;
100
- font-weight: 700;
101
- font-size: 14px;
102
- line-height: 1;
103
- padding: 0 4px;
104
- flex-shrink: 0;
105
- }
106
-
107
- .pd-dd-trigger-clear:hover {
108
- color: #444;
109
- }
110
-
111
- /* ── Multi-chip selection inside a non-tags trigger ──────────────────── */
112
-
113
- .pd-dd-chips {
114
- display: flex;
115
- flex-wrap: wrap;
116
- gap: 4px;
117
- flex: 1;
118
- min-width: 0;
119
- align-items: center;
120
- }
121
-
122
- /* Chip — replicates select2 `.select2-selection__choice` size & colours.
123
- * Order inside: <text> <×> — × on the right, opposite of older select2 but
124
- * matching modern select2 4.1+ and what users expect today.
125
- * `min-height: 24px` + `align-items: center` keeps chip content centered
126
- * vertically regardless of the typography of custom-rendered text. */
127
- .pd-dd-chip {
128
- display: inline-flex;
129
- align-items: center;
130
- background: #e4e4e4;
131
- color: #212529;
132
- border: 1px solid #aaa;
133
- border-radius: 4px;
134
- padding: 0 5px;
135
- font-size: 16px;
136
- line-height: 24px;
137
- min-height: 24px;
138
- max-width: 100%;
139
- }
140
-
141
- .pd-dd-chip-remove {
142
- display: inline-flex;
143
- align-items: center;
144
- justify-content: center;
145
- margin-left: 4px;
146
- color: #999;
147
- cursor: pointer;
148
- font-weight: 700;
149
- font-size: 14px;
150
- line-height: 1;
151
- }
152
-
153
- .pd-dd-chip-remove:hover {
154
- color: #444;
155
- }
156
-
157
- /* ── Panel (popup over trigger or always-visible in inline mode) ─────── */
158
-
159
- .pd-dd-panel {
160
- position: absolute;
161
- top: 100%;
162
- left: 0;
163
- right: 0;
164
- z-index: 1051;
165
- background: #fff;
166
- border: 1px solid #aaa;
167
- border-top: none;
168
- border-bottom-left-radius: 4px;
169
- border-bottom-right-radius: 4px;
170
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
171
- min-width: 200px;
172
- box-sizing: border-box;
173
- }
174
-
175
- .pd-dd-panel-inner {
176
- display: flex;
177
- flex-direction: column;
178
- max-height: 320px;
179
- }
180
-
181
- .pd-dd-search {
182
- padding: 4px;
183
- border-bottom: 1px solid #eee;
184
- }
185
-
186
- .pd-dd-search-input {
187
- width: 100%;
188
- padding: 4px;
189
- font-size: 14px;
190
- line-height: 1.4;
191
- border: 1px solid #aaa;
192
- border-radius: 0;
193
- box-sizing: border-box;
194
- outline: none;
195
- color: #212529;
196
- }
197
-
198
- .pd-dd-search-input:focus {
199
- border-color: #5897fb;
200
- }
201
-
202
- .pd-dd-results {
203
- overflow-y: auto;
204
- max-height: 240px;
205
- }
206
-
207
- .pd-dd-results-list {
208
- list-style: none;
209
- margin: 0;
210
- padding: 0;
211
- }
212
-
213
- /* Option row — same look across single, multi, multi-checkboxes, inline,
214
- * and tags modes. Differences (checkbox prefix, etc.) are layered on top via
215
- * additional classes — never via parent-mode-specific CSS. */
216
- .pd-dd-option {
217
- display: flex;
218
- align-items: center;
219
- gap: 6px;
220
- padding: 6px;
221
- cursor: pointer;
222
- font-size: 13px;
223
- line-height: 1.4;
224
- color: #212529;
225
- background: transparent;
226
- }
227
-
228
- .pd-dd-option--selected,
229
- .pd-dd-option--focused {
230
- background: #5897fb;
231
- color: #fff;
232
- }
233
-
234
- .pd-dd-option-text {
235
- flex: 1;
236
- min-width: 0;
237
- overflow: hidden;
238
- text-overflow: ellipsis;
239
- white-space: nowrap;
240
- }
241
-
242
- .pd-dd-no-results,
243
- .pd-dd-error {
244
- padding: 6px;
245
- color: #6c757d;
246
- font-style: italic;
247
- }
248
-
249
- .pd-dd-error {
250
- color: #b91c1c;
251
- font-style: normal;
252
- }
253
-
254
- /* `<LoadingIndicator>` brings its own overlay + min-height; the wrapping <li>
255
- * just needs to be a positioned block so the absolute overlay anchors here
256
- * (LoadingIndicator's ensureMinHeight already adds `position: relative`). */
257
- .pd-dd-loading {
258
- list-style: none;
259
- margin: 0;
260
- padding: 0;
261
- }
262
-
263
- /* Group header — bold, slightly indented, non-interactive */
264
- .pd-dd-group-header {
265
- padding: 6px;
266
- font-size: 13px;
267
- font-weight: 700;
268
- color: #212529;
269
- cursor: default;
270
- pointer-events: none;
271
- }
272
-
273
- /* Multi-checkboxes mode: each option row gets a Powerduck-style `form-check`
274
- * checkbox (NowUi skin, same markup as <CheckBox skin={NowUi} size={Small}>
275
- * everywhere else in the app). `pointer-events: none` makes the whole
276
- * checkbox visually-only — clicks pass through to the parent <li> which owns
277
- * the toggle path. The checkbox's `checked` attribute mirrors the row's
278
- * selection state via Vue's prop diffing on re-render. */
279
- .pd-dd-cb {
280
- display: inline-flex;
281
- align-items: center;
282
- margin: 0;
283
- padding: 0;
284
- pointer-events: none;
285
- flex-shrink: 0;
286
- }
287
-
288
- .pd-dd-cb .form-check-label {
289
- margin: 0;
290
- padding: 0;
291
- min-height: 0;
292
- cursor: inherit;
293
- }
294
-
295
- /* "Add: <typed>" suggestion row appears at the end of the tag-mode option list
296
- * when nothing matches the typed query. Visually consistent with normal rows. */
297
- .pd-dd-option-add {
298
- font-style: italic;
299
- }
300
-
301
- /* ── Header (OK button for Checkboxes mode) ──────────────────────────── */
302
-
303
- /* Mirrors select2's `.s2-cb-btn` from the s2-multi-cb plugin — single
304
- * commit button anchored at the top of the panel. Used by Checkboxes mode
305
- * only; multi-chip mode keeps Done/Cancel in the footer below. */
306
- .pd-dd-header {
307
- padding: 6px;
308
- border-bottom: 1px solid #eee;
309
- display: flex;
310
- justify-content: flex-end;
311
- }
312
-
313
- .pd-dd-header .btn {
314
- font-size: 13px;
315
- padding: 4px 12px;
316
- }
317
-
318
- /* ── Footer (Done button for chip-mode multi-select only) ────────────── */
319
-
320
- .pd-dd-footer {
321
- display: flex;
322
- justify-content: flex-end;
323
- gap: 6px;
324
- padding: 6px;
325
- border-top: 1px solid #eee;
326
- }
327
-
328
- .pd-dd-footer .btn {
329
- font-size: 13px;
330
- padding: 4px 10px;
331
- }
332
-
333
- /* ── Tags mode ───────────────────────────────────────────────────────── */
334
-
335
- .pd-dd-tags-root {
336
- width: 100%;
337
- }
338
-
339
- .pd-dd-tags-trigger {
340
- min-height: 38px;
341
- padding: 0;
342
- background: #fff;
343
- border: 1px solid #ced4da;
344
- border-radius: 4px;
345
- display: block;
346
- box-sizing: border-box;
347
- }
348
-
349
- /* Inner UL mirrors select2's `.select2-selection__rendered`:
350
- * horizontal padding 5px, no flex gap (each chip carries its own
351
- * `margin: 5px 5px 0 0` so chips align to the top of the trigger). */
352
- .pd-dd-tags-chips {
353
- list-style: none;
354
- margin: 0;
355
- padding: 0 5px;
356
- display: flex;
357
- flex-wrap: wrap;
358
- align-items: flex-start;
359
- min-height: 24px;
360
- }
361
-
362
- /* Tag chip — matches select2 `.select2-selection__choice` (16px/24px font,
363
- * 26px effective height, 5px top margin so chips sit just below the trigger
364
- * border). The 5px top margin (plus chip border + padding) brings each chip
365
- * to ~32px overall height, near-flush with the trigger's bottom edge. */
366
- .pd-dd-tag-chip {
367
- display: inline-flex;
368
- align-items: center;
369
- background: #e4e4e4;
370
- color: #212529;
371
- border: 1px solid #aaa;
372
- border-radius: 4px;
373
- padding: 0 5px;
374
- margin: 5px 5px 0 0;
375
- font-size: 16px;
376
- line-height: 24px;
377
- max-width: 100%;
378
- cursor: default;
379
- }
380
-
381
- .pd-dd-chip-action {
382
- display: inline-flex;
383
- align-items: center;
384
- justify-content: center;
385
- margin: 0 2px;
386
- color: #4b5563;
387
- cursor: pointer;
388
- font-size: 12px;
389
- padding: 0 2px;
390
- border-radius: 2px;
391
- }
392
-
393
- .pd-dd-chip-action:hover {
394
- background: #d9d9d9;
395
- color: #111827;
396
- }
397
-
398
- .pd-dd-tag-input-host {
399
- display: inline-flex;
400
- flex: 1 1 80px;
401
- min-width: 80px;
402
- margin-top: 5px;
403
- }
404
-
405
- /* Inline search input that doubles as the placeholder host when the chip
406
- * list is empty. Uses the same 16px/24px text size as chips so empty
407
- * placeholder text and any filled value sit on the same baseline. */
408
- .pd-dd-tag-input {
409
- border: none;
410
- outline: none;
411
- background: transparent;
412
- font-size: 16px;
413
- line-height: 24px;
414
- flex: 1;
415
- min-width: 80px;
416
- padding: 0;
417
- margin: 0;
418
- color: #212529;
419
- height: 24px;
420
- }
421
-
422
- .pd-dd-tag-input:disabled {
423
- cursor: not-allowed;
424
- }
425
-
426
- /* Placeholder colour matches `.pd-dd-trigger-text.pd-dd-placeholder` so the
427
- * empty-state look in tags mode is identical to single-select. */
428
- .pd-dd-tag-input::placeholder {
429
- color: #999;
430
- opacity: 1;
431
- }
432
-
433
- /* Sortable.js dragging affordance */
434
- .pd-dd-tag-chip.sortable-ghost {
435
- opacity: 0.4;
436
- }
437
-
438
- .pd-dd-tag-chip.sortable-chosen {
439
- background: #cbd5e0;
440
- border-color: #5897fb;
441
- }
442
-
443
- /* ── Inline mode ─────────────────────────────────────────────────────── */
444
-
445
- .pd-dd-inline {
446
- width: 100%;
447
- }
448
-
449
- .pd-dd-panel-inline {
450
- position: static;
451
- box-shadow: none;
452
- margin: 0;
453
- min-width: 0;
454
- }
455
-
456
- .pd-dd-panel-inline .pd-dd-panel-inner {
457
- max-height: none;
458
- }
459
-
460
- .pd-dd-panel-inline .pd-dd-results {
461
- max-height: none;
462
- }
463
-
464
- /* Migration sentinel: MIGRATION.md tells consumers to replace the deprecated
465
- * `afterBound` animation-disable hook with `cssClass="pd-dd-no-animation"`.
466
- * The base component currently has no open/close transitions, so the class
467
- * acts as a no-op marker today. Keep the selector here so any future
468
- * animation rules can opt out via `:not(.pd-dd-no-animation)` and the
469
- * promised migration target stays a real class. */
470
- .pd-dd-root.pd-dd-no-animation,
471
- .pd-dd-root.pd-dd-no-animation .pd-dd-panel {
472
- transition: none !important;
473
- animation: none !important;
474
- }
1
+ /* Styles for <DropdownNext>. Self-contained — the new dropdown emits no
2
+ .select2-* class aliases and is independent of select2's stylesheet.
3
+ The visual design (gray chips with × on the left, blue highlighted
4
+ options, bold group headers, form-control 38px trigger) replicates
5
+ select2's default theme but every rule below targets only .pd-dd-*
6
+ classes our TSX emits. Verified visually against select2 via
7
+ dev/validate-dropdown-parity.mjs. */
8
+
9
+ .pd-dd-root {
10
+ position: relative;
11
+ display: inline-block;
12
+ width: 100%;
13
+ }
14
+
15
+ /* ── Trigger (single & multi-chip, non-tags) ─────────────────────────── */
16
+
17
+ /* Outer trigger shell — keeps the project's form-control look (Bootstrap-y,
18
+ * #ced4da border, 38px min-height). Inner padding (8px left / 20px right)
19
+ * matches select2's `.select2-selection__rendered`, so text and chips sit at
20
+ * the same horizontal offset as in the legacy reference. */
21
+ .pd-dd-trigger {
22
+ display: flex;
23
+ align-items: center;
24
+ min-height: 38px;
25
+ padding: 4px 20px 4px 8px;
26
+ background: #fff;
27
+ border: 1px solid #ced4da;
28
+ border-radius: 4px;
29
+ cursor: pointer;
30
+ user-select: none;
31
+ position: relative;
32
+ box-sizing: border-box;
33
+ font-size: 14px;
34
+ color: #212529;
35
+ line-height: 1.4;
36
+ }
37
+
38
+ .pd-dd-trigger.pd-dd-disabled,
39
+ .pd-dd-trigger[aria-disabled="true"] {
40
+ background: #e9ecef;
41
+ cursor: not-allowed;
42
+ color: #6c757d;
43
+ }
44
+
45
+ .pd-dd-trigger:focus-visible {
46
+ outline: none;
47
+ border-color: #3b82f6;
48
+ }
49
+
50
+ .pd-dd-root.pd-dd-open .pd-dd-trigger {
51
+ border-bottom-left-radius: 0;
52
+ border-bottom-right-radius: 0;
53
+ }
54
+
55
+ .pd-dd-trigger-text {
56
+ flex: 1;
57
+ min-width: 0;
58
+ color: inherit;
59
+ white-space: nowrap;
60
+ overflow: hidden;
61
+ text-overflow: ellipsis;
62
+ }
63
+
64
+ .pd-dd-trigger-text.pd-dd-placeholder {
65
+ color: #999;
66
+ }
67
+
68
+ .pd-dd-trigger-arrow {
69
+ position: absolute;
70
+ right: 1px;
71
+ top: 1px;
72
+ bottom: 1px;
73
+ width: 20px;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ pointer-events: none;
78
+ }
79
+
80
+ .pd-dd-trigger-arrow b {
81
+ display: block;
82
+ width: 0;
83
+ height: 0;
84
+ border: 5px solid transparent;
85
+ border-top-color: #888;
86
+ border-bottom: 0;
87
+ }
88
+
89
+ /* Single-select clear button (× shown when placeholder is set and a value
90
+ * is selected; mirrors select2's allowClear behaviour). Sits on the right of
91
+ * the trigger text, just before the arrow — matches select2's clear-button
92
+ * placement. */
93
+ .pd-dd-trigger-clear {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ margin-left: 4px;
98
+ color: #999;
99
+ cursor: pointer;
100
+ font-weight: 700;
101
+ font-size: 14px;
102
+ line-height: 1;
103
+ padding: 0 4px;
104
+ flex-shrink: 0;
105
+ }
106
+
107
+ .pd-dd-trigger-clear:hover {
108
+ color: #444;
109
+ }
110
+
111
+ /* ── Multi-chip selection inside a non-tags trigger ──────────────────── */
112
+
113
+ .pd-dd-chips {
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ gap: 4px;
117
+ flex: 1;
118
+ min-width: 0;
119
+ align-items: center;
120
+ }
121
+
122
+ /* Chip — replicates select2 `.select2-selection__choice` size & colours.
123
+ * Order inside: <text> <×> — × on the right, opposite of older select2 but
124
+ * matching modern select2 4.1+ and what users expect today.
125
+ * `min-height: 24px` + `align-items: center` keeps chip content centered
126
+ * vertically regardless of the typography of custom-rendered text. */
127
+ .pd-dd-chip {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ background: #e4e4e4;
131
+ color: #212529;
132
+ border: 1px solid #aaa;
133
+ border-radius: 4px;
134
+ padding: 0 5px;
135
+ font-size: 16px;
136
+ line-height: 24px;
137
+ min-height: 24px;
138
+ max-width: 100%;
139
+ }
140
+
141
+ .pd-dd-chip-remove {
142
+ display: inline-flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ margin-left: 4px;
146
+ color: #999;
147
+ cursor: pointer;
148
+ font-weight: 700;
149
+ font-size: 14px;
150
+ line-height: 1;
151
+ }
152
+
153
+ .pd-dd-chip-remove:hover {
154
+ color: #444;
155
+ }
156
+
157
+ /* ── Panel (popup over trigger or always-visible in inline mode) ─────── */
158
+
159
+ .pd-dd-panel {
160
+ position: absolute;
161
+ top: 100%;
162
+ left: 0;
163
+ right: 0;
164
+ z-index: 1051;
165
+ background: #fff;
166
+ border: 1px solid #aaa;
167
+ border-top: none;
168
+ border-bottom-left-radius: 4px;
169
+ border-bottom-right-radius: 4px;
170
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
171
+ min-width: 200px;
172
+ box-sizing: border-box;
173
+ }
174
+
175
+ .pd-dd-panel-inner {
176
+ display: flex;
177
+ flex-direction: column;
178
+ max-height: 320px;
179
+ }
180
+
181
+ .pd-dd-search {
182
+ padding: 4px;
183
+ border-bottom: 1px solid #eee;
184
+ }
185
+
186
+ .pd-dd-search-input {
187
+ width: 100%;
188
+ padding: 4px;
189
+ font-size: 14px;
190
+ line-height: 1.4;
191
+ border: 1px solid #aaa;
192
+ border-radius: 0;
193
+ box-sizing: border-box;
194
+ outline: none;
195
+ color: #212529;
196
+ }
197
+
198
+ .pd-dd-search-input:focus {
199
+ border-color: #5897fb;
200
+ }
201
+
202
+ .pd-dd-results {
203
+ overflow-y: auto;
204
+ max-height: 240px;
205
+ }
206
+
207
+ .pd-dd-results-list {
208
+ list-style: none;
209
+ margin: 0;
210
+ padding: 0;
211
+ }
212
+
213
+ /* Option row — same look across single, multi, multi-checkboxes, inline,
214
+ * and tags modes. Differences (checkbox prefix, etc.) are layered on top via
215
+ * additional classes — never via parent-mode-specific CSS. */
216
+ .pd-dd-option {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 6px;
220
+ padding: 6px;
221
+ cursor: pointer;
222
+ font-size: 13px;
223
+ line-height: 1.4;
224
+ color: #212529;
225
+ background: transparent;
226
+ }
227
+
228
+ .pd-dd-option--selected,
229
+ .pd-dd-option--focused {
230
+ background: #5897fb;
231
+ color: #fff;
232
+ }
233
+
234
+ .pd-dd-option-text {
235
+ flex: 1;
236
+ min-width: 0;
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ white-space: nowrap;
240
+ }
241
+
242
+ .pd-dd-no-results,
243
+ .pd-dd-error {
244
+ padding: 6px;
245
+ color: #6c757d;
246
+ font-style: italic;
247
+ }
248
+
249
+ .pd-dd-error {
250
+ color: #b91c1c;
251
+ font-style: normal;
252
+ }
253
+
254
+ /* `<LoadingIndicator>` brings its own overlay + min-height; the wrapping <li>
255
+ * just needs to be a positioned block so the absolute overlay anchors here
256
+ * (LoadingIndicator's ensureMinHeight already adds `position: relative`). */
257
+ .pd-dd-loading {
258
+ list-style: none;
259
+ margin: 0;
260
+ padding: 0;
261
+ }
262
+
263
+ /* Group header — bold, slightly indented, non-interactive */
264
+ .pd-dd-group-header {
265
+ padding: 6px;
266
+ font-size: 13px;
267
+ font-weight: 700;
268
+ color: #212529;
269
+ cursor: default;
270
+ pointer-events: none;
271
+ }
272
+
273
+ /* Multi-checkboxes mode: each option row gets a Powerduck-style `form-check`
274
+ * checkbox (NowUi skin, same markup as <CheckBox skin={NowUi} size={Small}>
275
+ * everywhere else in the app). `pointer-events: none` makes the whole
276
+ * checkbox visually-only — clicks pass through to the parent <li> which owns
277
+ * the toggle path. The checkbox's `checked` attribute mirrors the row's
278
+ * selection state via Vue's prop diffing on re-render. */
279
+ .pd-dd-cb {
280
+ display: inline-flex;
281
+ align-items: center;
282
+ margin: 0;
283
+ padding: 0;
284
+ pointer-events: none;
285
+ flex-shrink: 0;
286
+ }
287
+
288
+ .pd-dd-cb .form-check-label {
289
+ margin: 0;
290
+ padding: 0;
291
+ min-height: 0;
292
+ cursor: inherit;
293
+ }
294
+
295
+ /* "Add: <typed>" suggestion row appears at the end of the tag-mode option list
296
+ * when nothing matches the typed query. Visually consistent with normal rows. */
297
+ .pd-dd-option-add {
298
+ font-style: italic;
299
+ }
300
+
301
+ /* ── Header (OK button for Checkboxes mode) ──────────────────────────── */
302
+
303
+ /* Mirrors select2's `.s2-cb-btn` from the s2-multi-cb plugin — single
304
+ * commit button anchored at the top of the panel. Used by Checkboxes mode
305
+ * only; multi-chip mode keeps Done/Cancel in the footer below. */
306
+ .pd-dd-header {
307
+ padding: 6px;
308
+ border-bottom: 1px solid #eee;
309
+ display: flex;
310
+ justify-content: flex-end;
311
+ }
312
+
313
+ .pd-dd-header .btn {
314
+ font-size: 13px;
315
+ padding: 4px 12px;
316
+ }
317
+
318
+ /* ── Footer (Done button for chip-mode multi-select only) ────────────── */
319
+
320
+ .pd-dd-footer {
321
+ display: flex;
322
+ justify-content: flex-end;
323
+ gap: 6px;
324
+ padding: 6px;
325
+ border-top: 1px solid #eee;
326
+ }
327
+
328
+ .pd-dd-footer .btn {
329
+ font-size: 13px;
330
+ padding: 4px 10px;
331
+ }
332
+
333
+ /* ── Tags mode ───────────────────────────────────────────────────────── */
334
+
335
+ .pd-dd-tags-root {
336
+ width: 100%;
337
+ }
338
+
339
+ .pd-dd-tags-trigger {
340
+ min-height: 38px;
341
+ padding: 0;
342
+ background: #fff;
343
+ border: 1px solid #ced4da;
344
+ border-radius: 4px;
345
+ display: block;
346
+ box-sizing: border-box;
347
+ }
348
+
349
+ /* Inner UL mirrors select2's `.select2-selection__rendered`:
350
+ * horizontal padding 5px, no flex gap (each chip carries its own
351
+ * `margin: 5px 5px 0 0` so chips align to the top of the trigger). */
352
+ .pd-dd-tags-chips {
353
+ list-style: none;
354
+ margin: 0;
355
+ padding: 0 5px;
356
+ display: flex;
357
+ flex-wrap: wrap;
358
+ align-items: flex-start;
359
+ min-height: 24px;
360
+ }
361
+
362
+ /* Tag chip — matches select2 `.select2-selection__choice` (16px/24px font,
363
+ * 26px effective height, 5px top margin so chips sit just below the trigger
364
+ * border). The 5px top margin (plus chip border + padding) brings each chip
365
+ * to ~32px overall height, near-flush with the trigger's bottom edge. */
366
+ .pd-dd-tag-chip {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ background: #e4e4e4;
370
+ color: #212529;
371
+ border: 1px solid #aaa;
372
+ border-radius: 4px;
373
+ padding: 0 5px;
374
+ margin: 5px 5px 0 0;
375
+ font-size: 16px;
376
+ line-height: 24px;
377
+ max-width: 100%;
378
+ cursor: default;
379
+ }
380
+
381
+ .pd-dd-chip-action {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ margin: 0 2px;
386
+ color: #4b5563;
387
+ cursor: pointer;
388
+ font-size: 12px;
389
+ padding: 0 2px;
390
+ border-radius: 2px;
391
+ }
392
+
393
+ .pd-dd-chip-action:hover {
394
+ background: #d9d9d9;
395
+ color: #111827;
396
+ }
397
+
398
+ .pd-dd-tag-input-host {
399
+ display: inline-flex;
400
+ flex: 1 1 80px;
401
+ min-width: 80px;
402
+ margin-top: 5px;
403
+ }
404
+
405
+ /* Inline search input that doubles as the placeholder host when the chip
406
+ * list is empty. Uses the same 16px/24px text size as chips so empty
407
+ * placeholder text and any filled value sit on the same baseline. */
408
+ .pd-dd-tag-input {
409
+ border: none;
410
+ outline: none;
411
+ background: transparent;
412
+ font-size: 16px;
413
+ line-height: 24px;
414
+ flex: 1;
415
+ min-width: 80px;
416
+ padding: 0;
417
+ margin: 0;
418
+ color: #212529;
419
+ height: 24px;
420
+ }
421
+
422
+ .pd-dd-tag-input:disabled {
423
+ cursor: not-allowed;
424
+ }
425
+
426
+ /* Placeholder colour matches `.pd-dd-trigger-text.pd-dd-placeholder` so the
427
+ * empty-state look in tags mode is identical to single-select. */
428
+ .pd-dd-tag-input::placeholder {
429
+ color: #999;
430
+ opacity: 1;
431
+ }
432
+
433
+ /* Sortable.js dragging affordance */
434
+ .pd-dd-tag-chip.sortable-ghost {
435
+ opacity: 0.4;
436
+ }
437
+
438
+ .pd-dd-tag-chip.sortable-chosen {
439
+ background: #cbd5e0;
440
+ border-color: #5897fb;
441
+ }
442
+
443
+ /* ── Inline mode ─────────────────────────────────────────────────────── */
444
+
445
+ .pd-dd-inline {
446
+ width: 100%;
447
+ }
448
+
449
+ .pd-dd-panel-inline {
450
+ position: static;
451
+ box-shadow: none;
452
+ margin: 0;
453
+ min-width: 0;
454
+ }
455
+
456
+ .pd-dd-panel-inline .pd-dd-panel-inner {
457
+ max-height: none;
458
+ }
459
+
460
+ .pd-dd-panel-inline .pd-dd-results {
461
+ max-height: none;
462
+ }
463
+
464
+ /* Migration sentinel: MIGRATION.md tells consumers to replace the deprecated
465
+ * `afterBound` animation-disable hook with `cssClass="pd-dd-no-animation"`.
466
+ * The base component currently has no open/close transitions, so the class
467
+ * acts as a no-op marker today. Keep the selector here so any future
468
+ * animation rules can opt out via `:not(.pd-dd-no-animation)` and the
469
+ * promised migration target stays a real class. */
470
+ .pd-dd-root.pd-dd-no-animation,
471
+ .pd-dd-root.pd-dd-no-animation .pd-dd-panel {
472
+ transition: none !important;
473
+ animation: none !important;
474
+ }
@@ -614,7 +614,10 @@ class DropdownListComponent extends TsxComponent<DropdownListArgs> implements Dr
614
614
 
615
615
  this.searchQuery = '';
616
616
  this.focusedFlatIndex = -1;
617
- if (this.multiselect === true) {
617
+ // Tags mode commits per click via toggleTagItem (emits `changed` directly).
618
+ // Initialising pendingMulti here would cause close()'s commit branch to
619
+ // re-emit the pre-toggle selection and override per-click commits — QA_GO-429 Bug A.
620
+ if (this.multiselect === true && this.tags !== true) {
618
621
  this.pendingMulti = [...this.getSelectedIds()];
619
622
  }
620
623
 
@@ -227,6 +227,18 @@ class NumericInputComponent extends TsxComponent<NumericInputArgs> implements Nu
227
227
  const renderedValue = applyDecimals ? newValue.toFixed(decimals) : (newValue ?? '');
228
228
 
229
229
  (this.$el as HTMLElement)?.querySelectorAll('input').forEach((input) => {
230
+ // Don't stomp the in-progress value while the user is typing. The Spinner-mode
231
+ // value interceptor + InputSpinner editor would otherwise re-format `input.value`
232
+ // to `Intl.NumberFormat(...)` then a follow-up re-render snaps it back to the
233
+ // stale prop — QA_GO-429 Bug B.
234
+ const active = (typeof document !== 'undefined' ? document.activeElement : null) as HTMLElement | null;
235
+ const isThisInputFocused = active === input
236
+ || (active != null && active.closest('.input-spinner-wrap')?.contains(input) === true);
237
+
238
+ if (isThisInputFocused) {
239
+ return;
240
+ }
241
+
230
242
  input.value = String(renderedValue);
231
243
  });
232
244
  }
@@ -146,7 +146,28 @@ export class InputSpinner {
146
146
  // Intercept direct writes to original.value (e.g. `input.value = '5'` from
147
147
  // Vue updates) so the visible spinner input stays in sync without callers
148
148
  // having to know about `setValue`.
149
+ //
150
+ // QA_GO-429 Bug B: short-circuit the callback when (a) the value the
151
+ // caller wrote is what the spinner already cached, OR (b) the write
152
+ // originated from the spinner's own `setValue()` (re-entry guard).
153
+ // Without these guards every `self.original.value = X` inside `setValue`
154
+ // re-queues another `setTimeout(setValue(X))`, an infinite setTimeout
155
+ // recursion that fires hundreds of times per second and clobbers the
156
+ // user's typed value every frame — see the Bug B trace in QA_GO-429.
149
157
  installValueInterceptor(self.original, (newVal) => {
158
+ if (self.inSetValue === true) {
159
+ return;
160
+ }
161
+
162
+ const parsed = parseFloat(newVal);
163
+ if (!isNaN(parsed) && parsed === self.value) {
164
+ return;
165
+ }
166
+
167
+ if ((newVal === '' || newVal == null) && isNaN(self.value)) {
168
+ return;
169
+ }
170
+
150
171
  setTimeout(() => setValue(newVal));
151
172
  });
152
173
 
@@ -231,22 +252,32 @@ export class InputSpinner {
231
252
  updateInput = true;
232
253
  }
233
254
 
234
- if (isNaN(newValue) || newValue === '') {
235
- self.original.value = '';
236
- if (updateInput) {
237
- self.input.value = '';
238
- }
255
+ // QA_GO-429 Bug B: mark re-entry so installValueInterceptor's callback
256
+ // skips the setTimeout(setValue) it would otherwise queue for every
257
+ // `self.original.value = ...` we do below. Without this flag the
258
+ // interceptor would queue an infinite setTimeout cascade that keeps
259
+ // firing setValue with stale values and clobbers user typing.
260
+ self.inSetValue = true;
261
+ try {
262
+ if (isNaN(newValue) || newValue === '') {
263
+ self.original.value = '';
264
+ if (updateInput) {
265
+ self.input.value = '';
266
+ }
239
267
 
240
- self.value = NaN;
241
- } else {
242
- newValue = parseFloat(newValue);
243
- newValue = Math.min(Math.max(newValue, self.min), self.max);
244
- self.original.value = newValue;
245
- if (updateInput) {
246
- self.input.value = (self.original as any).inputSpinnerEditor.render(newValue);
247
- }
268
+ self.value = NaN;
269
+ } else {
270
+ newValue = parseFloat(newValue);
271
+ newValue = Math.min(Math.max(newValue, self.min), self.max);
272
+ self.original.value = newValue;
273
+ if (updateInput) {
274
+ self.input.value = (self.original as any).inputSpinnerEditor.render(newValue);
275
+ }
248
276
 
249
- self.value = newValue;
277
+ self.value = newValue;
278
+ }
279
+ } finally {
280
+ self.inSetValue = false;
250
281
  }
251
282
  }
252
283
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "inviton-powerduck",
3
3
  "type": "module",
4
- "version": "0.0.321",
4
+ "version": "0.0.322",
5
5
  "files": [
6
6
  "app/",
7
7
  "common/",