@grayscale-dev/dragon 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -23,7 +23,7 @@ npm --workspace packages/ui run build:manifest
23
23
  npm --workspace packages/ui run test
24
24
  ```
25
25
 
26
- Covers value/property sync, native `input`/`change` events, form association, focus forwarding, styling hooks, and manifest integrity.
26
+ Covers value/property sync, native `input`/`change` events, form association, focus forwarding, styling hooks, masking behavior, and manifest integrity.
27
27
 
28
28
  **Usage (Vanilla)**
29
29
 
@@ -142,7 +142,28 @@ dui-input::part(input) {
142
142
 
143
143
  - `label-position="above"` (default): label sits above the field.
144
144
  - `label-position="floating"`: label sits like a placeholder and floats to the top-left on focus or when the input has a value.
145
- - Floating mode hides placeholder text to avoid overlap with the label.
145
+ - Floating mode hides placeholder text to avoid overlap with the label, unless regex placeholder mode is enabled while focused.
146
+
147
+ **Masking**
148
+
149
+ `regex` enables mask formatting using a supported regex-like pattern.
150
+
151
+ - Example phone pattern: `^\(\d{3}\)\s\d{3}-\d{4}$`
152
+ - As the user types, input is normalized to the mask shape.
153
+ - `regex-placeholder` provides explicit placeholder text for regex mode.
154
+ - If `regex-placeholder` is empty, no regex placeholder is shown.
155
+ - With `label-position="floating"`, regex placeholder appears only while focused.
156
+
157
+ Example:
158
+
159
+ ```html
160
+ <dui-input
161
+ label="Phone"
162
+ label-position="floating"
163
+ regex="^\(\d{3}\)\s\d{3}-\d{4}$"
164
+ regex-placeholder="(xxx) xxx-xxxx"
165
+ ></dui-input>
166
+ ```
146
167
 
147
168
  **Form Behavior**
148
169
 
@@ -11,21 +11,29 @@ export declare class DuiInput extends LitElement {
11
11
  autocomplete?: string;
12
12
  label: string;
13
13
  labelPosition: 'floating' | 'above';
14
+ regex: string;
15
+ regexPlaceholder: string;
14
16
  private inputEl?;
15
17
  private internals?;
16
18
  private defaultValue?;
19
+ private mask;
20
+ private isFocused;
17
21
  constructor();
18
22
  connectedCallback(): void;
23
+ willUpdate(changed: Map<string, unknown>): void;
19
24
  updated(changed: Map<string, unknown>): void;
20
25
  focus(options?: FocusOptions): void;
21
26
  blur(): void;
22
27
  formResetCallback(): void;
23
28
  formStateRestoreCallback(state: unknown): void;
24
29
  formDisabledCallback(disabled: boolean): void;
30
+ private normalizeMaskedValue;
25
31
  private syncFormValue;
26
32
  private handleInput;
27
33
  private handleChange;
28
34
  private handleKeydown;
35
+ private handleFocus;
36
+ private handleBlur;
29
37
  render(): import("lit-html").TemplateResult<1>;
30
38
  }
31
39
  declare global {
package/dist/index.js CHANGED
@@ -1,19 +1,107 @@
1
- import { css as h, LitElement as c, html as d } from "lit";
2
- import { property as l, query as f, customElement as b } from "lit/decorators.js";
3
- import { ifDefined as u } from "lit/directives/if-defined.js";
4
- var g = Object.defineProperty, v = Object.getOwnPropertyDescriptor, a = (t, i, n, r) => {
5
- for (var o = r > 1 ? void 0 : r ? v(i, n) : i, s = t.length - 1, p; s >= 0; s--)
6
- (p = t[s]) && (o = (r ? p(i, n, o) : p(o)) || o);
7
- return r && o && g(i, n, o), o;
1
+ import { css as $, LitElement as V, html as m } from "lit";
2
+ import { property as p, query as w, customElement as z } from "lit/decorators.js";
3
+ import { ifDefined as x } from "lit/directives/if-defined.js";
4
+ var P = Object.defineProperty, S = Object.getOwnPropertyDescriptor, r = (t, e, i, a) => {
5
+ for (var l = a > 1 ? void 0 : a ? S(e, i) : e, n = t.length - 1, o; n >= 0; n--)
6
+ (o = t[n]) && (l = (a ? o(e, i, l) : o(l)) || l);
7
+ return a && l && P(e, i, l), l;
8
8
  };
9
- let e = class extends c {
9
+ function E(t) {
10
+ const e = t.trim();
11
+ if (e.startsWith("/") && e.lastIndexOf("/") > 0) {
12
+ const i = e.lastIndexOf("/");
13
+ return e.slice(1, i);
14
+ }
15
+ return e;
16
+ }
17
+ function f(t, e) {
18
+ if (t[e] !== "{")
19
+ return { count: 1, nextIndex: e };
20
+ let i = e + 1, a = "";
21
+ for (; i < t.length && /\d/.test(t[i]); )
22
+ a += t[i], i += 1;
23
+ return !a || t[i] !== "}" ? { count: 1, nextIndex: e } : {
24
+ count: Number(a),
25
+ nextIndex: i + 1
26
+ };
27
+ }
28
+ function g(t, e, i) {
29
+ const a = Number.isFinite(i) && i > 0 ? Math.floor(i) : 1;
30
+ for (let l = 0; l < a; l += 1)
31
+ e.kind === "slot" ? t.push({ kind: "slot", test: e.test, placeholder: e.placeholder }) : t.push({ kind: "literal", value: e.value });
32
+ }
33
+ function k(t) {
34
+ if (!t.trim()) return null;
35
+ let e = E(t);
36
+ if (e.startsWith("^") && (e = e.slice(1)), e.endsWith("$") && (e = e.slice(0, -1)), !e) return null;
37
+ const i = [];
38
+ for (let l = 0; l < e.length; l += 1) {
39
+ const n = e[l];
40
+ if (n === "\\") {
41
+ const u = e[l + 1];
42
+ if (!u) return null;
43
+ let d;
44
+ u === "d" ? d = { kind: "slot", test: (c) => /^\d$/.test(c), placeholder: "x" } : u === "w" ? d = { kind: "slot", test: (c) => /^\w$/.test(c), placeholder: "x" } : u === "s" ? d = { kind: "literal", value: " " } : d = { kind: "literal", value: u };
45
+ const h = f(e, l + 2);
46
+ g(i, d, h.count), l = h.nextIndex - 1;
47
+ continue;
48
+ }
49
+ if (n === "[") {
50
+ const u = e.indexOf("]", l + 1);
51
+ if (u === -1) return null;
52
+ const d = e.slice(l + 1, u);
53
+ if (!d) return null;
54
+ const h = new RegExp(`^[${d}]$`), c = {
55
+ kind: "slot",
56
+ test: (y) => h.test(y),
57
+ placeholder: "x"
58
+ }, v = f(e, u + 1);
59
+ g(i, c, v.count), l = v.nextIndex - 1;
60
+ continue;
61
+ }
62
+ if (n === ".") {
63
+ const u = {
64
+ kind: "slot",
65
+ test: (h) => /^[\s\S]$/.test(h),
66
+ placeholder: "x"
67
+ }, d = f(e, l + 1);
68
+ g(i, u, d.count), l = d.nextIndex - 1;
69
+ continue;
70
+ }
71
+ if (n === "+" || n === "*" || n === "?" || n === "{" || n === "}")
72
+ return null;
73
+ const o = { kind: "literal", value: n }, b = f(e, l + 1);
74
+ g(i, o, b.count), l = b.nextIndex - 1;
75
+ }
76
+ const a = i.map((l) => l.kind === "literal" ? l.value : l.placeholder).join("");
77
+ return {
78
+ source: e,
79
+ tokens: i,
80
+ placeholder: a
81
+ };
82
+ }
83
+ let s = class extends V {
10
84
  constructor() {
11
- super(), this.value = "", this.placeholder = "", this.name = "", this.disabled = !1, this.required = !1, this.type = "text", this.label = "", this.labelPosition = "above", "attachInternals" in this && (this.internals = this.attachInternals());
85
+ super(), this.value = "", this.placeholder = "", this.name = "", this.disabled = !1, this.required = !1, this.type = "text", this.label = "", this.labelPosition = "above", this.regex = "", this.regexPlaceholder = "", this.mask = null, this.isFocused = !1, "attachInternals" in this && (this.internals = this.attachInternals());
12
86
  }
13
87
  connectedCallback() {
14
- super.connectedCallback(), this.defaultValue === void 0 && (this.defaultValue = this.getAttribute("value") ?? ""), this.syncFormValue();
88
+ if (super.connectedCallback(), this.mask = k(this.regex), this.defaultValue === void 0) {
89
+ const t = this.getAttribute("value") ?? "";
90
+ this.defaultValue = this.normalizeMaskedValue(t);
91
+ }
92
+ this.value = this.normalizeMaskedValue(this.value), this.syncFormValue();
93
+ }
94
+ willUpdate(t) {
95
+ t.has("regex") && (this.mask = k(this.regex), this.defaultValue = this.normalizeMaskedValue(this.getAttribute("value") ?? ""));
15
96
  }
16
97
  updated(t) {
98
+ if (t.has("value") || t.has("regex")) {
99
+ const e = this.normalizeMaskedValue(this.value);
100
+ if (e !== this.value) {
101
+ this.value = e;
102
+ return;
103
+ }
104
+ }
17
105
  (t.has("value") || t.has("disabled")) && this.syncFormValue();
18
106
  }
19
107
  focus(t) {
@@ -26,11 +114,31 @@ let e = class extends c {
26
114
  this.value = this.defaultValue ?? "";
27
115
  }
28
116
  formStateRestoreCallback(t) {
29
- typeof t == "string" && (this.value = t);
117
+ typeof t == "string" && (this.value = this.normalizeMaskedValue(t));
30
118
  }
31
119
  formDisabledCallback(t) {
32
120
  this.disabled = t;
33
121
  }
122
+ normalizeMaskedValue(t) {
123
+ if (!this.mask) return t;
124
+ const e = this.mask.tokens.filter(
125
+ (o) => o.kind === "slot"
126
+ ), i = [];
127
+ let a = 0;
128
+ for (const o of t) {
129
+ if (a >= e.length) break;
130
+ e[a].test(o) && (i.push(o), a += 1);
131
+ }
132
+ let l = "", n = 0;
133
+ for (const o of this.mask.tokens)
134
+ if (o.kind === "slot") {
135
+ if (n >= i.length)
136
+ break;
137
+ l += i[n], n += 1;
138
+ } else
139
+ i.length > 0 && n <= i.length && (l += o.value);
140
+ return l;
141
+ }
34
142
  syncFormValue() {
35
143
  if (this.internals) {
36
144
  if (this.disabled) {
@@ -42,24 +150,33 @@ let e = class extends c {
42
150
  }
43
151
  handleInput(t) {
44
152
  t.stopPropagation();
45
- const i = t.target;
46
- this.value = i.value, this.dispatchEvent(new Event("input", { bubbles: !0, composed: !0 }));
153
+ const e = t.target, i = this.normalizeMaskedValue(e.value);
154
+ e.value !== i && (e.value = i), this.value = i, this.dispatchEvent(new Event("input", { bubbles: !0, composed: !0 }));
47
155
  }
48
156
  handleChange(t) {
49
157
  t.stopPropagation();
50
- const i = t.target;
51
- this.value = i.value, this.dispatchEvent(new Event("change", { bubbles: !0, composed: !0 }));
158
+ const e = t.target, i = this.normalizeMaskedValue(e.value);
159
+ e.value !== i && (e.value = i), this.value = i, this.dispatchEvent(new Event("change", { bubbles: !0, composed: !0 }));
52
160
  }
53
161
  handleKeydown(t) {
54
162
  if (t.key !== "Enter") return;
55
- const i = t.target;
56
- this.value = i.value, this.dispatchEvent(new Event("change", { bubbles: !0, composed: !0 }));
163
+ const e = t.target, i = this.normalizeMaskedValue(e.value);
164
+ e.value !== i && (e.value = i), this.value = i, this.dispatchEvent(new Event("change", { bubbles: !0, composed: !0 }));
165
+ }
166
+ handleFocus() {
167
+ this.isFocused = !0, this.requestUpdate();
168
+ }
169
+ handleBlur() {
170
+ this.isFocused = !1, this.requestUpdate();
57
171
  }
58
172
  render() {
59
- const t = this.label.length > 0, i = t && this.labelPosition === "floating", n = this.value.length > 0, r = i ? void 0 : this.placeholder || void 0;
60
- return d`
61
- <div class="field ${i ? "floating" : ""}" data-has-value=${n}>
62
- ${t ? d`<label for="input">${this.label}</label>` : null}
173
+ const t = this.label.length > 0, e = t && this.labelPosition === "floating", i = this.value.length > 0, a = this.regexPlaceholder.trim() || void 0;
174
+ let l;
175
+ e ? l = a && this.isFocused ? a : void 0 : a ? l = a : l = this.placeholder || void 0;
176
+ const n = this.label || l || this.placeholder || a || void 0;
177
+ return m`
178
+ <div class="field ${e ? "floating" : ""}" data-has-value=${i}>
179
+ ${t ? m`<label for="input">${this.label}</label>` : null}
63
180
  <input
64
181
  id="input"
65
182
  part="input"
@@ -68,9 +185,11 @@ let e = class extends c {
68
185
  .name=${this.name}
69
186
  ?disabled=${this.disabled}
70
187
  ?required=${this.required}
71
- placeholder=${u(r)}
72
- autocomplete=${u(this.autocomplete)}
73
- aria-label=${u(this.label || this.placeholder || void 0)}
188
+ .placeholder=${l ?? ""}
189
+ autocomplete=${x(this.autocomplete)}
190
+ aria-label=${x(n)}
191
+ @focus=${this.handleFocus}
192
+ @blur=${this.handleBlur}
74
193
  @input=${this.handleInput}
75
194
  @change=${this.handleChange}
76
195
  @keydown=${this.handleKeydown}
@@ -79,8 +198,8 @@ let e = class extends c {
79
198
  `;
80
199
  }
81
200
  };
82
- e.formAssociated = !0;
83
- e.styles = h`
201
+ s.formAssociated = !0;
202
+ s.styles = $`
84
203
  :host {
85
204
  display: inline-block;
86
205
  width: 100%;
@@ -116,7 +235,7 @@ e.styles = h`
116
235
 
117
236
  .field.floating[data-has-value='true'] label,
118
237
  :host(:focus-within) .field.floating label {
119
- top: 7px;
238
+ top: 4px;
120
239
  transform: scale(0.85);
121
240
  color: var(--ui-input-label-color, #475569);
122
241
  }
@@ -138,10 +257,6 @@ e.styles = h`
138
257
  color: var(--ui-input-placeholder-color, #9aa4b2);
139
258
  }
140
259
 
141
- .field.floating input::placeholder {
142
- color: transparent;
143
- }
144
-
145
260
  input:focus {
146
261
  box-shadow: var(--ui-input-focus-ring, 0 0 0 3px rgba(24, 98, 255, 0.25));
147
262
  }
@@ -158,39 +273,45 @@ e.styles = h`
158
273
  padding-left: var(--ui-input-floating-padding-left, 12px);
159
274
  }
160
275
  `;
161
- a([
162
- l({ type: String })
163
- ], e.prototype, "value", 2);
164
- a([
165
- l({ type: String })
166
- ], e.prototype, "placeholder", 2);
167
- a([
168
- l({ type: String, reflect: !0 })
169
- ], e.prototype, "name", 2);
170
- a([
171
- l({ type: Boolean, reflect: !0 })
172
- ], e.prototype, "disabled", 2);
173
- a([
174
- l({ type: Boolean, reflect: !0 })
175
- ], e.prototype, "required", 2);
176
- a([
177
- l({ type: String, reflect: !0 })
178
- ], e.prototype, "type", 2);
179
- a([
180
- l({ type: String, reflect: !0 })
181
- ], e.prototype, "autocomplete", 2);
182
- a([
183
- l({ type: String })
184
- ], e.prototype, "label", 2);
185
- a([
186
- l({ type: String, attribute: "label-position" })
187
- ], e.prototype, "labelPosition", 2);
188
- a([
189
- f("input")
190
- ], e.prototype, "inputEl", 2);
191
- e = a([
192
- b("dui-input")
193
- ], e);
276
+ r([
277
+ p({ type: String })
278
+ ], s.prototype, "value", 2);
279
+ r([
280
+ p({ type: String })
281
+ ], s.prototype, "placeholder", 2);
282
+ r([
283
+ p({ type: String, reflect: !0 })
284
+ ], s.prototype, "name", 2);
285
+ r([
286
+ p({ type: Boolean, reflect: !0 })
287
+ ], s.prototype, "disabled", 2);
288
+ r([
289
+ p({ type: Boolean, reflect: !0 })
290
+ ], s.prototype, "required", 2);
291
+ r([
292
+ p({ type: String, reflect: !0 })
293
+ ], s.prototype, "type", 2);
294
+ r([
295
+ p({ type: String, reflect: !0 })
296
+ ], s.prototype, "autocomplete", 2);
297
+ r([
298
+ p({ type: String })
299
+ ], s.prototype, "label", 2);
300
+ r([
301
+ p({ type: String, attribute: "label-position" })
302
+ ], s.prototype, "labelPosition", 2);
303
+ r([
304
+ p({ type: String, reflect: !0 })
305
+ ], s.prototype, "regex", 2);
306
+ r([
307
+ p({ type: String, attribute: "regex-placeholder" })
308
+ ], s.prototype, "regexPlaceholder", 2);
309
+ r([
310
+ w("input")
311
+ ], s.prototype, "inputEl", 2);
312
+ s = r([
313
+ z("dui-input")
314
+ ], s);
194
315
  export {
195
- e as DuiInput
316
+ s as DuiInput
196
317
  };
@@ -17,7 +17,21 @@
17
17
  },
18
18
  {
19
19
  "name": "placeholder",
20
- "description": "Placeholder text shown when value is empty. Hidden in floating label mode.",
20
+ "description": "Placeholder text shown when value is empty. Hidden in floating label mode unless explicit regex placeholder mode is enabled while focused.",
21
+ "type": "string",
22
+ "default": "",
23
+ "control": "text"
24
+ },
25
+ {
26
+ "name": "regex",
27
+ "description": "Regex-like mask definition. Supported patterns include escaped literals and quantified slots such as ^\\(\\d{3}\\)\\s\\d{3}-\\d{4}$.",
28
+ "type": "string",
29
+ "default": "",
30
+ "control": "text"
31
+ },
32
+ {
33
+ "name": "regex-placeholder",
34
+ "description": "Explicit placeholder text for regex mode. If omitted, regex placeholder is not shown.",
21
35
  "type": "string",
22
36
  "default": "",
23
37
  "control": "text"
@@ -90,6 +104,18 @@
90
104
  "type": "string",
91
105
  "default": ""
92
106
  },
107
+ {
108
+ "name": "regex",
109
+ "description": "Mask source pattern used to constrain input formatting.",
110
+ "type": "string",
111
+ "default": ""
112
+ },
113
+ {
114
+ "name": "regexPlaceholder",
115
+ "description": "Explicit placeholder text used when regex-placeholder is provided.",
116
+ "type": "string",
117
+ "default": ""
118
+ },
93
119
  {
94
120
  "name": "labelPosition",
95
121
  "description": "Reflects the label presentation mode.",
@@ -243,35 +269,41 @@
243
269
  "description": "Native input behavior options.",
244
270
  "order": 2
245
271
  },
272
+ {
273
+ "id": "mask",
274
+ "label": "Mask",
275
+ "description": "Regex masking controls.",
276
+ "order": 3
277
+ },
246
278
  {
247
279
  "id": "typography",
248
280
  "label": "Typography",
249
- "order": 3
281
+ "order": 4
250
282
  },
251
283
  {
252
284
  "id": "spacing",
253
285
  "label": "Spacing",
254
- "order": 4
286
+ "order": 5
255
287
  },
256
288
  {
257
289
  "id": "floating",
258
290
  "label": "Floating Label",
259
- "order": 5
291
+ "order": 6
260
292
  },
261
293
  {
262
294
  "id": "shape",
263
295
  "label": "Shape",
264
- "order": 6
296
+ "order": 7
265
297
  },
266
298
  {
267
299
  "id": "color",
268
300
  "label": "Color",
269
- "order": 7
301
+ "order": 8
270
302
  },
271
303
  {
272
304
  "id": "state",
273
305
  "label": "States",
274
- "order": 8
306
+ "order": 9
275
307
  }
276
308
  ],
277
309
  "controls": [
@@ -331,6 +363,22 @@
331
363
  "label": "Required",
332
364
  "order": 4
333
365
  },
366
+ {
367
+ "id": "regex",
368
+ "kind": "attribute",
369
+ "ref": "regex",
370
+ "group": "mask",
371
+ "label": "Regex Mask",
372
+ "order": 1
373
+ },
374
+ {
375
+ "id": "regex-placeholder",
376
+ "kind": "attribute",
377
+ "ref": "regex-placeholder",
378
+ "group": "mask",
379
+ "label": "Regex Placeholder",
380
+ "order": 2
381
+ },
334
382
  {
335
383
  "id": "token-font-size",
336
384
  "kind": "cssToken",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grayscale-dev/dragon",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Framework-agnostic Web Components built with Lit.",
5
5
  "type": "module",
6
6
  "files": [