@deepfuture/dui-components 0.0.14 → 0.0.16

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.
@@ -44,6 +44,7 @@ import { base } from "@deepfuture/dui-core/base";
44
44
  import { customEvent } from "@deepfuture/dui-core/event";
45
45
  import { fieldContext } from "@deepfuture/dui-components/field";
46
46
  export const valueChangeEvent = customEvent("value-change", { bubbles: true, composed: true });
47
+ export const valueCommittedEvent = customEvent("value-committed", { bubbles: true, composed: true });
47
48
  /** Structural styles only — layout CSS. */
48
49
  const styles = css `
49
50
  :host {
@@ -51,8 +52,40 @@ const styles = css `
51
52
  }
52
53
 
53
54
  [part="root"] {
54
- display: inline-flex;
55
+ display: flex;
55
56
  align-items: center;
57
+ width: 100%;
58
+ position: relative;
59
+ box-sizing: border-box;
60
+ user-select: none;
61
+ -webkit-tap-highlight-color: transparent;
62
+ transition-property: background, box-shadow, border-color, color, opacity;
63
+ }
64
+
65
+ [part="root"][data-scrub] {
66
+ cursor: ew-resize;
67
+ }
68
+
69
+ [part="root"][data-disabled] {
70
+ pointer-events: none;
71
+ }
72
+
73
+ [part="label"] {
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ [part="label"][data-scrub] {
81
+ cursor: ew-resize;
82
+ }
83
+
84
+ [part="icon"] {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ flex-shrink: 0;
56
89
  }
57
90
 
58
91
  [part="input"] {
@@ -62,32 +95,21 @@ const styles = css `
62
95
  background: none;
63
96
  font: inherit;
64
97
  color: inherit;
65
- text-align: center;
66
98
  min-width: 0;
99
+ flex: 1;
100
+ }
101
+
102
+ [part="input"][data-scrub] {
103
+ cursor: ew-resize;
67
104
  }
68
105
 
69
106
  [part="input"]:disabled {
70
107
  cursor: not-allowed;
71
108
  }
72
109
 
73
- [part="decrement"],
74
- [part="increment"] {
75
- display: inline-flex;
76
- align-items: center;
77
- justify-content: center;
78
- cursor: pointer;
79
- border: none;
80
- background: none;
81
- padding: 0;
82
- margin: 0;
83
- font: inherit;
84
- color: inherit;
110
+ [part="unit"] {
85
111
  flex-shrink: 0;
86
- }
87
-
88
- [part="decrement"]:disabled,
89
- [part="increment"]:disabled {
90
- cursor: not-allowed;
112
+ pointer-events: none;
91
113
  }
92
114
 
93
115
  .HiddenInput {
@@ -99,14 +121,21 @@ const styles = css `
99
121
  height: 0;
100
122
  }
101
123
  `;
124
+ /** Drag threshold in px before scrub starts. */
125
+ const DRAG_THRESHOLD = 3;
102
126
  /**
103
- * `<dui-number-field>` — A numeric input with increment/decrement buttons.
127
+ * `<dui-number-field>` — A numeric input with optional label, icon,
128
+ * unit suffix, drag-to-scrub behavior, and precision formatting.
129
+ *
130
+ * For simple step-up/step-down with buttons, use `<dui-stepper>` instead.
104
131
  *
105
132
  * @csspart root - The outer container.
133
+ * @csspart label - Label text element.
134
+ * @csspart icon - Icon slot wrapper.
106
135
  * @csspart input - The text input element.
107
- * @csspart decrement - The decrement button.
108
- * @csspart increment - The increment button.
136
+ * @csspart unit - Unit suffix element.
109
137
  * @fires value-change - Fired when value changes. Detail: { value: number }
138
+ * @fires value-committed - Fired on pointerup (end of drag), blur, or Enter. Detail: { value: number }
110
139
  */
111
140
  let DuiNumberField = (() => {
112
141
  let _classSuper = LitElement;
@@ -140,6 +169,39 @@ let DuiNumberField = (() => {
140
169
  let _name_decorators;
141
170
  let _name_initializers = [];
142
171
  let _name_extraInitializers = [];
172
+ let _label_decorators;
173
+ let _label_initializers = [];
174
+ let _label_extraInitializers = [];
175
+ let _labelPosition_decorators;
176
+ let _labelPosition_initializers = [];
177
+ let _labelPosition_extraInitializers = [];
178
+ let _iconPosition_decorators;
179
+ let _iconPosition_initializers = [];
180
+ let _iconPosition_extraInitializers = [];
181
+ let _unit_decorators;
182
+ let _unit_initializers = [];
183
+ let _unit_extraInitializers = [];
184
+ let _precision_decorators;
185
+ let _precision_initializers = [];
186
+ let _precision_extraInitializers = [];
187
+ let _scrubLabel_decorators;
188
+ let _scrubLabel_initializers = [];
189
+ let _scrubLabel_extraInitializers = [];
190
+ let _scrubValue_decorators;
191
+ let _scrubValue_initializers = [];
192
+ let _scrubValue_extraInitializers = [];
193
+ let _scrubField_decorators;
194
+ let _scrubField_initializers = [];
195
+ let _scrubField_extraInitializers = [];
196
+ let _clickLabel_decorators;
197
+ let _clickLabel_initializers = [];
198
+ let _clickLabel_extraInitializers = [];
199
+ let _clickValue_decorators;
200
+ let _clickValue_initializers = [];
201
+ let _clickValue_extraInitializers = [];
202
+ let _clickField_decorators;
203
+ let _clickField_initializers = [];
204
+ let _clickField_extraInitializers = [];
143
205
  let _private_internalValue_decorators;
144
206
  let _private_internalValue_initializers = [];
145
207
  let _private_internalValue_extraInitializers = [];
@@ -148,6 +210,14 @@ let DuiNumberField = (() => {
148
210
  let _private_inputText_initializers = [];
149
211
  let _private_inputText_extraInitializers = [];
150
212
  let _private_inputText_descriptor;
213
+ let _private_dragging_decorators;
214
+ let _private_dragging_initializers = [];
215
+ let _private_dragging_extraInitializers = [];
216
+ let _private_dragging_descriptor;
217
+ let _private_editing_decorators;
218
+ let _private_editing_initializers = [];
219
+ let _private_editing_extraInitializers = [];
220
+ let _private_editing_descriptor;
151
221
  let __fieldCtx_decorators;
152
222
  let __fieldCtx_initializers = [];
153
223
  let __fieldCtx_extraInitializers = [];
@@ -164,8 +234,21 @@ let DuiNumberField = (() => {
164
234
  _readOnly_decorators = [property({ type: Boolean, reflect: true, attribute: "read-only" })];
165
235
  _required_decorators = [property({ type: Boolean })];
166
236
  _name_decorators = [property()];
237
+ _label_decorators = [property({ reflect: true })];
238
+ _labelPosition_decorators = [property({ reflect: true, attribute: "label-position" })];
239
+ _iconPosition_decorators = [property({ reflect: true, attribute: "icon-position" })];
240
+ _unit_decorators = [property({ reflect: true })];
241
+ _precision_decorators = [property({ type: Number })];
242
+ _scrubLabel_decorators = [property({ type: Boolean, reflect: true, attribute: "scrub-label" })];
243
+ _scrubValue_decorators = [property({ type: Boolean, reflect: true, attribute: "scrub-value" })];
244
+ _scrubField_decorators = [property({ type: Boolean, reflect: true, attribute: "scrub-field" })];
245
+ _clickLabel_decorators = [property({ type: Boolean, reflect: true, attribute: "click-label" })];
246
+ _clickValue_decorators = [property({ type: Boolean, reflect: true, attribute: "click-value" })];
247
+ _clickField_decorators = [property({ type: Boolean, reflect: true, attribute: "click-field" })];
167
248
  _private_internalValue_decorators = [state()];
168
249
  _private_inputText_decorators = [state()];
250
+ _private_dragging_decorators = [state()];
251
+ _private_editing_decorators = [state()];
169
252
  __fieldCtx_decorators = [consume({ context: fieldContext, subscribe: true }), state()];
170
253
  __esDecorate(this, null, _value_decorators, { kind: "accessor", name: "value", static: false, private: false, access: { has: obj => "value" in obj, get: obj => obj.value, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, _value_initializers, _value_extraInitializers);
171
254
  __esDecorate(this, null, _defaultValue_decorators, { kind: "accessor", name: "defaultValue", static: false, private: false, access: { has: obj => "defaultValue" in obj, get: obj => obj.defaultValue, set: (obj, value) => { obj.defaultValue = value; } }, metadata: _metadata }, _defaultValue_initializers, _defaultValue_extraInitializers);
@@ -177,8 +260,21 @@ let DuiNumberField = (() => {
177
260
  __esDecorate(this, null, _readOnly_decorators, { kind: "accessor", name: "readOnly", static: false, private: false, access: { has: obj => "readOnly" in obj, get: obj => obj.readOnly, set: (obj, value) => { obj.readOnly = value; } }, metadata: _metadata }, _readOnly_initializers, _readOnly_extraInitializers);
178
261
  __esDecorate(this, null, _required_decorators, { kind: "accessor", name: "required", static: false, private: false, access: { has: obj => "required" in obj, get: obj => obj.required, set: (obj, value) => { obj.required = value; } }, metadata: _metadata }, _required_initializers, _required_extraInitializers);
179
262
  __esDecorate(this, null, _name_decorators, { kind: "accessor", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
263
+ __esDecorate(this, null, _label_decorators, { kind: "accessor", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
264
+ __esDecorate(this, null, _labelPosition_decorators, { kind: "accessor", name: "labelPosition", static: false, private: false, access: { has: obj => "labelPosition" in obj, get: obj => obj.labelPosition, set: (obj, value) => { obj.labelPosition = value; } }, metadata: _metadata }, _labelPosition_initializers, _labelPosition_extraInitializers);
265
+ __esDecorate(this, null, _iconPosition_decorators, { kind: "accessor", name: "iconPosition", static: false, private: false, access: { has: obj => "iconPosition" in obj, get: obj => obj.iconPosition, set: (obj, value) => { obj.iconPosition = value; } }, metadata: _metadata }, _iconPosition_initializers, _iconPosition_extraInitializers);
266
+ __esDecorate(this, null, _unit_decorators, { kind: "accessor", name: "unit", static: false, private: false, access: { has: obj => "unit" in obj, get: obj => obj.unit, set: (obj, value) => { obj.unit = value; } }, metadata: _metadata }, _unit_initializers, _unit_extraInitializers);
267
+ __esDecorate(this, null, _precision_decorators, { kind: "accessor", name: "precision", static: false, private: false, access: { has: obj => "precision" in obj, get: obj => obj.precision, set: (obj, value) => { obj.precision = value; } }, metadata: _metadata }, _precision_initializers, _precision_extraInitializers);
268
+ __esDecorate(this, null, _scrubLabel_decorators, { kind: "accessor", name: "scrubLabel", static: false, private: false, access: { has: obj => "scrubLabel" in obj, get: obj => obj.scrubLabel, set: (obj, value) => { obj.scrubLabel = value; } }, metadata: _metadata }, _scrubLabel_initializers, _scrubLabel_extraInitializers);
269
+ __esDecorate(this, null, _scrubValue_decorators, { kind: "accessor", name: "scrubValue", static: false, private: false, access: { has: obj => "scrubValue" in obj, get: obj => obj.scrubValue, set: (obj, value) => { obj.scrubValue = value; } }, metadata: _metadata }, _scrubValue_initializers, _scrubValue_extraInitializers);
270
+ __esDecorate(this, null, _scrubField_decorators, { kind: "accessor", name: "scrubField", static: false, private: false, access: { has: obj => "scrubField" in obj, get: obj => obj.scrubField, set: (obj, value) => { obj.scrubField = value; } }, metadata: _metadata }, _scrubField_initializers, _scrubField_extraInitializers);
271
+ __esDecorate(this, null, _clickLabel_decorators, { kind: "accessor", name: "clickLabel", static: false, private: false, access: { has: obj => "clickLabel" in obj, get: obj => obj.clickLabel, set: (obj, value) => { obj.clickLabel = value; } }, metadata: _metadata }, _clickLabel_initializers, _clickLabel_extraInitializers);
272
+ __esDecorate(this, null, _clickValue_decorators, { kind: "accessor", name: "clickValue", static: false, private: false, access: { has: obj => "clickValue" in obj, get: obj => obj.clickValue, set: (obj, value) => { obj.clickValue = value; } }, metadata: _metadata }, _clickValue_initializers, _clickValue_extraInitializers);
273
+ __esDecorate(this, null, _clickField_decorators, { kind: "accessor", name: "clickField", static: false, private: false, access: { has: obj => "clickField" in obj, get: obj => obj.clickField, set: (obj, value) => { obj.clickField = value; } }, metadata: _metadata }, _clickField_initializers, _clickField_extraInitializers);
180
274
  __esDecorate(this, _private_internalValue_descriptor = { get: __setFunctionName(function () { return this.#internalValue_accessor_storage; }, "#internalValue", "get"), set: __setFunctionName(function (value) { this.#internalValue_accessor_storage = value; }, "#internalValue", "set") }, _private_internalValue_decorators, { kind: "accessor", name: "#internalValue", static: false, private: true, access: { has: obj => #internalValue in obj, get: obj => obj.#internalValue, set: (obj, value) => { obj.#internalValue = value; } }, metadata: _metadata }, _private_internalValue_initializers, _private_internalValue_extraInitializers);
181
275
  __esDecorate(this, _private_inputText_descriptor = { get: __setFunctionName(function () { return this.#inputText_accessor_storage; }, "#inputText", "get"), set: __setFunctionName(function (value) { this.#inputText_accessor_storage = value; }, "#inputText", "set") }, _private_inputText_decorators, { kind: "accessor", name: "#inputText", static: false, private: true, access: { has: obj => #inputText in obj, get: obj => obj.#inputText, set: (obj, value) => { obj.#inputText = value; } }, metadata: _metadata }, _private_inputText_initializers, _private_inputText_extraInitializers);
276
+ __esDecorate(this, _private_dragging_descriptor = { get: __setFunctionName(function () { return this.#dragging_accessor_storage; }, "#dragging", "get"), set: __setFunctionName(function (value) { this.#dragging_accessor_storage = value; }, "#dragging", "set") }, _private_dragging_decorators, { kind: "accessor", name: "#dragging", static: false, private: true, access: { has: obj => #dragging in obj, get: obj => obj.#dragging, set: (obj, value) => { obj.#dragging = value; } }, metadata: _metadata }, _private_dragging_initializers, _private_dragging_extraInitializers);
277
+ __esDecorate(this, _private_editing_descriptor = { get: __setFunctionName(function () { return this.#editing_accessor_storage; }, "#editing", "get"), set: __setFunctionName(function (value) { this.#editing_accessor_storage = value; }, "#editing", "set") }, _private_editing_decorators, { kind: "accessor", name: "#editing", static: false, private: true, access: { has: obj => #editing in obj, get: obj => obj.#editing, set: (obj, value) => { obj.#editing = value; } }, metadata: _metadata }, _private_editing_initializers, _private_editing_extraInitializers);
182
278
  __esDecorate(this, null, __fieldCtx_decorators, { kind: "accessor", name: "_fieldCtx", static: false, private: false, access: { has: obj => "_fieldCtx" in obj, get: obj => obj._fieldCtx, set: (obj, value) => { obj._fieldCtx = value; } }, metadata: _metadata }, __fieldCtx_initializers, __fieldCtx_extraInitializers);
183
279
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
184
280
  }
@@ -189,6 +285,7 @@ let DuiNumberField = (() => {
189
285
  };
190
286
  static styles = [base, styles];
191
287
  #value_accessor_storage = __runInitializers(this, _value_initializers, undefined);
288
+ // ── Core properties ────────────────────────────────────────────────
192
289
  get value() { return this.#value_accessor_storage; }
193
290
  set value(value) { this.#value_accessor_storage = value; }
194
291
  #defaultValue_accessor_storage = (__runInitializers(this, _value_extraInitializers), __runInitializers(this, _defaultValue_initializers, undefined));
@@ -218,15 +315,64 @@ let DuiNumberField = (() => {
218
315
  #name_accessor_storage = (__runInitializers(this, _required_extraInitializers), __runInitializers(this, _name_initializers, undefined));
219
316
  get name() { return this.#name_accessor_storage; }
220
317
  set name(value) { this.#name_accessor_storage = value; }
221
- #internalValue_accessor_storage = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _private_internalValue_initializers, undefined));
318
+ #label_accessor_storage = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _label_initializers, ""));
319
+ // ── Display properties ─────────────────────────────────────────────
320
+ get label() { return this.#label_accessor_storage; }
321
+ set label(value) { this.#label_accessor_storage = value; }
322
+ #labelPosition_accessor_storage = (__runInitializers(this, _label_extraInitializers), __runInitializers(this, _labelPosition_initializers, ""));
323
+ get labelPosition() { return this.#labelPosition_accessor_storage; }
324
+ set labelPosition(value) { this.#labelPosition_accessor_storage = value; }
325
+ #iconPosition_accessor_storage = (__runInitializers(this, _labelPosition_extraInitializers), __runInitializers(this, _iconPosition_initializers, ""));
326
+ get iconPosition() { return this.#iconPosition_accessor_storage; }
327
+ set iconPosition(value) { this.#iconPosition_accessor_storage = value; }
328
+ #unit_accessor_storage = (__runInitializers(this, _iconPosition_extraInitializers), __runInitializers(this, _unit_initializers, ""));
329
+ get unit() { return this.#unit_accessor_storage; }
330
+ set unit(value) { this.#unit_accessor_storage = value; }
331
+ #precision_accessor_storage = (__runInitializers(this, _unit_extraInitializers), __runInitializers(this, _precision_initializers, undefined));
332
+ get precision() { return this.#precision_accessor_storage; }
333
+ set precision(value) { this.#precision_accessor_storage = value; }
334
+ #scrubLabel_accessor_storage = (__runInitializers(this, _precision_extraInitializers), __runInitializers(this, _scrubLabel_initializers, false));
335
+ // ── Interaction zone booleans ──────────────────────────────────────
336
+ get scrubLabel() { return this.#scrubLabel_accessor_storage; }
337
+ set scrubLabel(value) { this.#scrubLabel_accessor_storage = value; }
338
+ #scrubValue_accessor_storage = (__runInitializers(this, _scrubLabel_extraInitializers), __runInitializers(this, _scrubValue_initializers, false));
339
+ get scrubValue() { return this.#scrubValue_accessor_storage; }
340
+ set scrubValue(value) { this.#scrubValue_accessor_storage = value; }
341
+ #scrubField_accessor_storage = (__runInitializers(this, _scrubValue_extraInitializers), __runInitializers(this, _scrubField_initializers, false));
342
+ get scrubField() { return this.#scrubField_accessor_storage; }
343
+ set scrubField(value) { this.#scrubField_accessor_storage = value; }
344
+ #clickLabel_accessor_storage = (__runInitializers(this, _scrubField_extraInitializers), __runInitializers(this, _clickLabel_initializers, false));
345
+ get clickLabel() { return this.#clickLabel_accessor_storage; }
346
+ set clickLabel(value) { this.#clickLabel_accessor_storage = value; }
347
+ #clickValue_accessor_storage = (__runInitializers(this, _clickLabel_extraInitializers), __runInitializers(this, _clickValue_initializers, false));
348
+ get clickValue() { return this.#clickValue_accessor_storage; }
349
+ set clickValue(value) { this.#clickValue_accessor_storage = value; }
350
+ #clickField_accessor_storage = (__runInitializers(this, _clickValue_extraInitializers), __runInitializers(this, _clickField_initializers, false));
351
+ get clickField() { return this.#clickField_accessor_storage; }
352
+ set clickField(value) { this.#clickField_accessor_storage = value; }
353
+ #internalValue_accessor_storage = (__runInitializers(this, _clickField_extraInitializers), __runInitializers(this, _private_internalValue_initializers, undefined));
354
+ // ── Internal state ─────────────────────────────────────────────────
222
355
  get #internalValue() { return _private_internalValue_descriptor.get.call(this); }
223
356
  set #internalValue(value) { return _private_internalValue_descriptor.set.call(this, value); }
224
357
  #inputText_accessor_storage = (__runInitializers(this, _private_internalValue_extraInitializers), __runInitializers(this, _private_inputText_initializers, ""));
225
358
  get #inputText() { return _private_inputText_descriptor.get.call(this); }
226
359
  set #inputText(value) { return _private_inputText_descriptor.set.call(this, value); }
227
- #_fieldCtx_accessor_storage = (__runInitializers(this, _private_inputText_extraInitializers), __runInitializers(this, __fieldCtx_initializers, void 0));
360
+ #dragging_accessor_storage = (__runInitializers(this, _private_inputText_extraInitializers), __runInitializers(this, _private_dragging_initializers, false));
361
+ get #dragging() { return _private_dragging_descriptor.get.call(this); }
362
+ set #dragging(value) { return _private_dragging_descriptor.set.call(this, value); }
363
+ #editing_accessor_storage = (__runInitializers(this, _private_dragging_extraInitializers), __runInitializers(this, _private_editing_initializers, false));
364
+ get #editing() { return _private_editing_descriptor.get.call(this); }
365
+ set #editing(value) { return _private_editing_descriptor.set.call(this, value); }
366
+ #_fieldCtx_accessor_storage = (__runInitializers(this, _private_editing_extraInitializers), __runInitializers(this, __fieldCtx_initializers, void 0));
228
367
  get _fieldCtx() { return this.#_fieldCtx_accessor_storage; }
229
368
  set _fieldCtx(value) { this.#_fieldCtx_accessor_storage = value; }
369
+ // ── Drag state (not reactive) ──────────────────────────────────────
370
+ #dragStartX = (__runInitializers(this, __fieldCtx_extraInitializers), 0);
371
+ #dragStartValue = 0;
372
+ #dragStarted = false;
373
+ #dragPointerId = null;
374
+ #dragZoneAllowsClick = false;
375
+ // ── Computed getters ───────────────────────────────────────────────
230
376
  get #currentValue() {
231
377
  return this.value ?? this.#internalValue;
232
378
  }
@@ -236,18 +382,68 @@ let DuiNumberField = (() => {
236
382
  get #isInvalid() {
237
383
  return this._fieldCtx?.invalid ?? false;
238
384
  }
239
- get #canDecrement() {
240
- const v = this.#currentValue;
241
- if (v === undefined)
242
- return true;
243
- return this.min === undefined || v > this.min;
385
+ get #inferredPrecision() {
386
+ const stepStr = String(this.step);
387
+ const dotIndex = stepStr.indexOf(".");
388
+ if (dotIndex === -1)
389
+ return 0;
390
+ return stepStr.length - dotIndex - 1;
244
391
  }
245
- get #canIncrement() {
392
+ get #displayValue() {
246
393
  const v = this.#currentValue;
247
394
  if (v === undefined)
248
- return true;
249
- return this.max === undefined || v < this.max;
395
+ return "";
396
+ const p = this.precision ?? this.#inferredPrecision;
397
+ return v.toFixed(p);
398
+ }
399
+ /** Whether any interaction boolean is explicitly set by the consumer. */
400
+ get #hasExplicitInteraction() {
401
+ return (this.scrubLabel ||
402
+ this.scrubValue ||
403
+ this.scrubField ||
404
+ this.clickLabel ||
405
+ this.clickValue ||
406
+ this.clickField);
407
+ }
408
+ /** Effective scrub-label: explicit or default. */
409
+ get #effectiveScrubLabel() {
410
+ if (this.#hasExplicitInteraction)
411
+ return this.scrubLabel;
412
+ // Default: scrub on label when a label is present
413
+ return this.label !== "";
250
414
  }
415
+ /** Effective scrub-value: explicit or default. */
416
+ get #effectiveScrubValue() {
417
+ if (this.#hasExplicitInteraction)
418
+ return this.scrubValue;
419
+ return false;
420
+ }
421
+ /** Effective scrub-field: explicit or default. */
422
+ get #effectiveScrubField() {
423
+ if (this.#hasExplicitInteraction)
424
+ return this.scrubField;
425
+ // Default: scrub on field when no label
426
+ return this.label === "";
427
+ }
428
+ /** Effective click-label: explicit or default. */
429
+ get #effectiveClickLabel() {
430
+ if (this.#hasExplicitInteraction)
431
+ return this.clickLabel;
432
+ return false;
433
+ }
434
+ /** Effective click-value: explicit or default. */
435
+ get #effectiveClickValue() {
436
+ if (this.#hasExplicitInteraction)
437
+ return this.clickValue;
438
+ return true;
439
+ }
440
+ /** Effective click-field: explicit or default. */
441
+ get #effectiveClickField() {
442
+ if (this.#hasExplicitInteraction)
443
+ return this.clickField;
444
+ return false;
445
+ }
446
+ // ── Lifecycle ──────────────────────────────────────────────────────
251
447
  connectedCallback() {
252
448
  super.connectedCallback();
253
449
  if (this.value === undefined && this.defaultValue !== undefined) {
@@ -256,11 +452,13 @@ let DuiNumberField = (() => {
256
452
  this.#syncInputText();
257
453
  }
258
454
  willUpdate() {
259
- this.#syncInputText();
455
+ if (!this.#editing) {
456
+ this.#syncInputText();
457
+ }
260
458
  }
459
+ // ── Value helpers ──────────────────────────────────────────────────
261
460
  #syncInputText() {
262
- const v = this.#currentValue;
263
- this.#inputText = v !== undefined ? String(v) : "";
461
+ this.#inputText = this.#displayValue;
264
462
  }
265
463
  #clamp(val) {
266
464
  if (this.min !== undefined)
@@ -278,12 +476,12 @@ let DuiNumberField = (() => {
278
476
  this._fieldCtx?.setFilled(true);
279
477
  this.dispatchEvent(valueChangeEvent({ value: clamped }));
280
478
  }
281
- #increment = (__runInitializers(this, __fieldCtx_extraInitializers), (amount) => {
479
+ #increment = (amount) => {
282
480
  if (this.#isDisabled || this.readOnly)
283
481
  return;
284
482
  const current = this.#currentValue ?? this.min ?? 0;
285
483
  this.#setValue(current + amount);
286
- });
484
+ };
287
485
  #decrement = (amount) => {
288
486
  if (this.#isDisabled || this.readOnly)
289
487
  return;
@@ -298,27 +496,179 @@ let DuiNumberField = (() => {
298
496
  else {
299
497
  this.#setValue(parsed);
300
498
  }
499
+ this.#editing = false;
500
+ const v = this.#currentValue;
501
+ if (v !== undefined) {
502
+ this.dispatchEvent(valueCommittedEvent({ value: v }));
503
+ }
504
+ }
505
+ #focusInputAndSelectAll() {
506
+ const input = this.shadowRoot?.querySelector('[part="input"]');
507
+ if (input) {
508
+ this.#editing = true;
509
+ input.focus();
510
+ input.select();
511
+ }
512
+ }
513
+ // ── Drag-to-scrub ─────────────────────────────────────────────────
514
+ #startDrag(e, allowsClick) {
515
+ if (this.#isDisabled || this.readOnly)
516
+ return;
517
+ this.#dragPointerId = e.pointerId;
518
+ this.#dragStartX = e.clientX;
519
+ this.#dragStartValue = this.#currentValue ?? 0;
520
+ this.#dragStarted = false;
521
+ this.#dragZoneAllowsClick = allowsClick;
522
+ this.setPointerCapture(e.pointerId);
523
+ this.addEventListener("pointermove", this.#onPointerMove);
524
+ this.addEventListener("pointerup", this.#onPointerUp);
525
+ this.addEventListener("pointercancel", this.#onPointerUp);
301
526
  }
527
+ #onPointerMove = (e) => {
528
+ if (e.pointerId !== this.#dragPointerId)
529
+ return;
530
+ const deltaX = e.clientX - this.#dragStartX;
531
+ if (!this.#dragStarted) {
532
+ if (Math.abs(deltaX) < DRAG_THRESHOLD)
533
+ return;
534
+ this.#dragStarted = true;
535
+ this.#dragging = true;
536
+ }
537
+ let sensitivity = this.step;
538
+ if (e.shiftKey) {
539
+ sensitivity = this.step * 0.1;
540
+ }
541
+ else if (e.ctrlKey || e.metaKey) {
542
+ sensitivity = this.largeStep;
543
+ }
544
+ const newValue = this.#dragStartValue + deltaX * sensitivity;
545
+ this.#setValue(newValue);
546
+ };
547
+ #onPointerUp = (e) => {
548
+ if (e.pointerId !== this.#dragPointerId)
549
+ return;
550
+ this.releasePointerCapture(e.pointerId);
551
+ this.removeEventListener("pointermove", this.#onPointerMove);
552
+ this.removeEventListener("pointerup", this.#onPointerUp);
553
+ this.removeEventListener("pointercancel", this.#onPointerUp);
554
+ if (this.#dragStarted) {
555
+ this.#dragging = false;
556
+ const v = this.#currentValue;
557
+ if (v !== undefined) {
558
+ this.dispatchEvent(valueCommittedEvent({ value: v }));
559
+ }
560
+ }
561
+ else if (this.#dragZoneAllowsClick) {
562
+ this.#focusInputAndSelectAll();
563
+ }
564
+ this.#dragPointerId = null;
565
+ };
566
+ // ── Zone pointer handlers ──────────────────────────────────────────
567
+ #onLabelPointerDown = (e) => {
568
+ if (e.button !== 0)
569
+ return;
570
+ // Field-wide flags apply to all zones
571
+ const allowsScrub = this.#effectiveScrubLabel || this.#effectiveScrubField;
572
+ const allowsClick = this.#effectiveClickLabel || this.#effectiveClickField;
573
+ if (allowsScrub && allowsClick) {
574
+ e.preventDefault();
575
+ this.#startDrag(e, true);
576
+ }
577
+ else if (allowsScrub) {
578
+ e.preventDefault();
579
+ this.#startDrag(e, false);
580
+ }
581
+ else if (allowsClick) {
582
+ e.preventDefault();
583
+ this.#focusInputAndSelectAll();
584
+ }
585
+ };
586
+ #onInputPointerDown = (e) => {
587
+ if (e.button !== 0)
588
+ return;
589
+ // Field-wide flags apply to all zones
590
+ const allowsScrub = this.#effectiveScrubValue || this.#effectiveScrubField;
591
+ const allowsClick = this.#effectiveClickValue || this.#effectiveClickField;
592
+ if (allowsScrub && allowsClick) {
593
+ e.preventDefault();
594
+ this.#startDrag(e, true);
595
+ }
596
+ else if (allowsScrub) {
597
+ e.preventDefault();
598
+ this.#startDrag(e, false);
599
+ }
600
+ else if (allowsClick) {
601
+ this.#editing = true;
602
+ }
603
+ };
604
+ #onRootPointerDown = (e) => {
605
+ if (e.button !== 0)
606
+ return;
607
+ // Root handler only fires for clicks on the root background itself
608
+ const rootEl = this.shadowRoot?.querySelector('[part="root"]');
609
+ if (e.target !== rootEl)
610
+ return;
611
+ const allowsScrub = this.#effectiveScrubField;
612
+ const allowsClick = this.#effectiveClickField;
613
+ if (allowsScrub && allowsClick) {
614
+ e.preventDefault();
615
+ this.#startDrag(e, true);
616
+ }
617
+ else if (allowsScrub) {
618
+ e.preventDefault();
619
+ this.#startDrag(e, false);
620
+ }
621
+ else if (allowsClick) {
622
+ e.preventDefault();
623
+ this.#focusInputAndSelectAll();
624
+ }
625
+ };
626
+ // ── Input event handlers ───────────────────────────────────────────
302
627
  #onInput = (e) => {
303
628
  this.#inputText = e.target.value;
304
629
  };
305
630
  #onBlur = () => {
306
- this.#commitInput();
631
+ if (this.#editing) {
632
+ this.#commitInput();
633
+ }
307
634
  this._fieldCtx?.setFocused(false);
308
635
  this._fieldCtx?.markTouched();
309
636
  };
310
637
  #onFocus = () => {
638
+ this.#editing = true;
311
639
  this._fieldCtx?.setFocused(true);
640
+ const input = this.shadowRoot?.querySelector('[part="input"]');
641
+ if (input) {
642
+ requestAnimationFrame(() => input.select());
643
+ }
312
644
  };
313
645
  #onKeyDown = (e) => {
314
646
  switch (e.key) {
315
647
  case "ArrowUp":
316
648
  e.preventDefault();
317
- this.#increment(e.shiftKey ? this.largeStep : this.step);
649
+ if (e.shiftKey) {
650
+ this.#increment(this.step * 0.1);
651
+ }
652
+ else if (e.ctrlKey || e.metaKey) {
653
+ this.#increment(this.largeStep);
654
+ }
655
+ else {
656
+ this.#increment(this.step);
657
+ }
658
+ this.#syncInputText();
318
659
  break;
319
660
  case "ArrowDown":
320
661
  e.preventDefault();
321
- this.#decrement(e.shiftKey ? this.largeStep : this.step);
662
+ if (e.shiftKey) {
663
+ this.#decrement(this.step * 0.1);
664
+ }
665
+ else if (e.ctrlKey || e.metaKey) {
666
+ this.#decrement(this.largeStep);
667
+ }
668
+ else {
669
+ this.#decrement(this.step);
670
+ }
671
+ this.#syncInputText();
322
672
  break;
323
673
  case "Home":
324
674
  if (this.min !== undefined) {
@@ -335,61 +685,71 @@ let DuiNumberField = (() => {
335
685
  case "Enter":
336
686
  this.#commitInput();
337
687
  break;
688
+ case "Escape": {
689
+ this.#editing = false;
690
+ this.#syncInputText();
691
+ const input = this.shadowRoot?.querySelector('[part="input"]');
692
+ if (input)
693
+ input.blur();
694
+ break;
695
+ }
338
696
  }
339
697
  };
340
- #onDecrementClick = () => {
341
- this.#decrement(this.step);
342
- };
343
- #onIncrementClick = () => {
344
- this.#increment(this.step);
345
- };
698
+ // ── Render ─────────────────────────────────────────────────────────
346
699
  render() {
347
700
  const isDisabled = this.#isDisabled;
348
701
  const isInvalid = this.#isInvalid;
349
702
  const controlId = this._fieldCtx?.controlId ?? "";
350
703
  const currentValue = this.#currentValue;
704
+ // Compute which zones are scrubbable for cursor styling
705
+ const labelScrub = this.#effectiveScrubLabel || this.#effectiveScrubField;
706
+ const inputScrub = this.#effectiveScrubValue || this.#effectiveScrubField;
707
+ const rootScrub = this.#effectiveScrubField;
351
708
  return html `
709
+ <span
710
+ part="label"
711
+ ?data-scrub="${labelScrub}"
712
+ @pointerdown="${this.#onLabelPointerDown}"
713
+ >${this.label}</span>
714
+
352
715
  <div
353
716
  part="root"
717
+ ?data-scrub="${rootScrub}"
718
+ ?data-dragging="${this.#dragging}"
354
719
  ?data-disabled="${isDisabled}"
720
+ ?data-readonly="${this.readOnly}"
355
721
  ?data-invalid="${isInvalid}"
722
+ @pointerdown="${this.#onRootPointerDown}"
356
723
  >
357
- <button
358
- part="decrement"
359
- type="button"
360
- tabindex="-1"
361
- aria-label="Decrease"
362
- ?disabled="${isDisabled || this.readOnly || !this.#canDecrement}"
363
- @click="${this.#onDecrementClick}"
364
- >
365
- <slot name="decrement">&minus;</slot>
366
- </button>
724
+ <span part="icon">
725
+ <slot name="icon"></slot>
726
+ </span>
727
+
367
728
  <input
368
729
  part="input"
369
730
  id="${controlId || nothing}"
370
731
  type="text"
371
732
  inputmode="decimal"
733
+ ?data-scrub="${inputScrub}"
372
734
  .value="${live(this.#inputText)}"
373
735
  ?disabled="${isDisabled}"
374
736
  ?readonly="${this.readOnly}"
375
737
  ?required="${this.required}"
738
+ aria-label="${this.label || nothing}"
739
+ aria-valuenow="${currentValue ?? nothing}"
740
+ aria-valuemin="${this.min ?? nothing}"
741
+ aria-valuemax="${this.max ?? nothing}"
376
742
  aria-invalid="${isInvalid ? "true" : nothing}"
377
743
  ?data-disabled="${isDisabled}"
744
+ @pointerdown="${this.#onInputPointerDown}"
378
745
  @input="${this.#onInput}"
379
746
  @keydown="${this.#onKeyDown}"
380
747
  @focus="${this.#onFocus}"
381
748
  @blur="${this.#onBlur}"
382
749
  />
383
- <button
384
- part="increment"
385
- type="button"
386
- tabindex="-1"
387
- aria-label="Increase"
388
- ?disabled="${isDisabled || this.readOnly || !this.#canIncrement}"
389
- @click="${this.#onIncrementClick}"
390
- >
391
- <slot name="increment">+</slot>
392
- </button>
750
+
751
+ <span part="unit">${this.unit}</span>
752
+
393
753
  ${this.name
394
754
  ? html `<input
395
755
  type="hidden"