@sken-ds/primitives 0.3.1 → 0.3.4
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/dist/sken-input.js +50 -26
- package/dist/sken-input.js.map +1 -1
- package/dist/sken-number-input.js +67 -43
- package/dist/sken-number-input.js.map +1 -1
- package/dist/sken-textarea.js +40 -16
- package/dist/sken-textarea.js.map +1 -1
- package/package.json +1 -1
package/dist/sken-input.js
CHANGED
|
@@ -6,8 +6,12 @@ var o, s = class extends t {
|
|
|
6
6
|
static {
|
|
7
7
|
o = this;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
static {
|
|
10
|
+
this.formAssociated = !0;
|
|
11
|
+
}
|
|
12
|
+
#e;
|
|
13
|
+
constructor() {
|
|
14
|
+
super(), this.#e = null, this.type = "text", this.size = "md", this.value = void 0, this.defaultValue = void 0, this.placeholder = void 0, this.disabled = !1, this.readonly = !1, this.required = !1, this.invalid = !1, this.describedById = void 0, this.name = void 0, this.autocomplete = void 0, this.maxLength = void 0, this.minLength = void 0, this.pattern = void 0, this.#t = "", this.#r = () => {
|
|
11
15
|
requestAnimationFrame(() => {
|
|
12
16
|
requestAnimationFrame(() => {
|
|
13
17
|
let e = this.renderRoot.querySelector("input");
|
|
@@ -16,35 +20,40 @@ var o, s = class extends t {
|
|
|
16
20
|
e.style.paddingInlineStart = r > 0 ? `calc(${r}px + ${o.SLOT_AIR})` : "", e.style.paddingInlineEnd = i > 0 ? `calc(${i}px + ${o.SLOT_AIR})` : "";
|
|
17
21
|
});
|
|
18
22
|
});
|
|
19
|
-
}, this.#
|
|
23
|
+
}, this.#i = null, this.#a = null, this.#o = null, this.#s = (e) => {
|
|
20
24
|
let t = e.target.value;
|
|
21
|
-
this.value === void 0 && (this.#
|
|
25
|
+
this.value === void 0 && (this.#t = t), this.#n(), this.dispatchEvent(new CustomEvent("sken-input", {
|
|
22
26
|
detail: t,
|
|
23
27
|
bubbles: !0,
|
|
24
28
|
composed: !0
|
|
25
29
|
}));
|
|
26
|
-
}, this.#
|
|
30
|
+
}, this.#c = (e) => {
|
|
27
31
|
let t = e.target;
|
|
28
|
-
this.dispatchEvent(new CustomEvent("sken-change", {
|
|
32
|
+
this.#n(), this.dispatchEvent(new CustomEvent("sken-change", {
|
|
29
33
|
detail: t.value,
|
|
30
34
|
bubbles: !0,
|
|
31
35
|
composed: !0
|
|
32
36
|
}));
|
|
33
|
-
}, this.#
|
|
37
|
+
}, this.#l = (e) => {
|
|
34
38
|
this.dispatchEvent(new CustomEvent("sken-focus", {
|
|
35
39
|
detail: e,
|
|
36
40
|
bubbles: !0,
|
|
37
41
|
composed: !0
|
|
38
42
|
}));
|
|
39
|
-
}, this.#
|
|
43
|
+
}, this.#u = (e) => {
|
|
40
44
|
this.dispatchEvent(new CustomEvent("sken-blur", {
|
|
41
45
|
detail: e,
|
|
42
46
|
bubbles: !0,
|
|
43
47
|
composed: !0
|
|
44
48
|
}));
|
|
45
49
|
};
|
|
50
|
+
try {
|
|
51
|
+
this.#e = this.attachInternals();
|
|
52
|
+
} catch {
|
|
53
|
+
this.#e = null;
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
|
-
#
|
|
56
|
+
#t;
|
|
48
57
|
static {
|
|
49
58
|
this.styles = n`
|
|
50
59
|
:host {
|
|
@@ -154,37 +163,52 @@ var o, s = class extends t {
|
|
|
154
163
|
`;
|
|
155
164
|
}
|
|
156
165
|
connectedCallback() {
|
|
157
|
-
super.connectedCallback(), this.value === void 0 && (this.#
|
|
166
|
+
super.connectedCallback(), this.value === void 0 && (this.#t = this.defaultValue ?? ""), this.#n();
|
|
158
167
|
}
|
|
159
168
|
disconnectedCallback() {
|
|
160
|
-
super.disconnectedCallback(), this.#
|
|
169
|
+
super.disconnectedCallback(), this.#i?.disconnect(), this.#a?.disconnect(), this.#o?.disconnect();
|
|
170
|
+
}
|
|
171
|
+
formResetCallback() {
|
|
172
|
+
if (this.value === void 0) {
|
|
173
|
+
this.#t = this.defaultValue ?? "";
|
|
174
|
+
let e = this.renderRoot.querySelector("input");
|
|
175
|
+
e && (e.value = this.#t), this.#n();
|
|
176
|
+
} else this.requestUpdate();
|
|
177
|
+
}
|
|
178
|
+
formDisabledCallback(e) {
|
|
179
|
+
this.disabled = e;
|
|
180
|
+
}
|
|
181
|
+
#n() {
|
|
182
|
+
if (!this.#e) return;
|
|
183
|
+
let e = this.value ?? this.#t;
|
|
184
|
+
this.#e.setFormValue(e);
|
|
161
185
|
}
|
|
162
186
|
firstUpdated() {
|
|
163
187
|
let e = this.renderRoot.querySelector(".slot-start"), t = this.renderRoot.querySelector(".slot-end"), n = this.renderRoot.querySelector("input");
|
|
164
188
|
if (!e || !t || !n) return;
|
|
165
|
-
this.#
|
|
189
|
+
this.#i = new MutationObserver(() => this.#r()), this.#i.observe(e, {
|
|
166
190
|
childList: !0,
|
|
167
191
|
subtree: !0,
|
|
168
192
|
attributes: !0
|
|
169
|
-
}), this.#
|
|
193
|
+
}), this.#a = new MutationObserver(() => this.#r()), this.#a.observe(t, {
|
|
170
194
|
childList: !0,
|
|
171
195
|
subtree: !0,
|
|
172
196
|
attributes: !0
|
|
173
|
-
}), this.#
|
|
174
|
-
for (let t of e) t.isIntersecting && this.#
|
|
175
|
-
}), this.#
|
|
197
|
+
}), this.#o = new IntersectionObserver((e) => {
|
|
198
|
+
for (let t of e) t.isIntersecting && this.#r();
|
|
199
|
+
}), this.#o.observe(this);
|
|
176
200
|
let r = this.renderRoot.querySelector("slot[name=\"start\"]"), i = this.renderRoot.querySelector("slot[name=\"end\"]");
|
|
177
|
-
r?.addEventListener("slotchange", this.#
|
|
201
|
+
r?.addEventListener("slotchange", this.#r), i?.addEventListener("slotchange", this.#r), this.#r();
|
|
178
202
|
}
|
|
179
203
|
static {
|
|
180
204
|
this.SLOT_AIR = "0.5rem";
|
|
181
205
|
}
|
|
182
|
-
#t;
|
|
183
|
-
#n;
|
|
184
206
|
#r;
|
|
185
207
|
#i;
|
|
208
|
+
#a;
|
|
209
|
+
#o;
|
|
186
210
|
render() {
|
|
187
|
-
let e = this.value ?? this.#
|
|
211
|
+
let e = this.value ?? this.#t;
|
|
188
212
|
return r`
|
|
189
213
|
<div class="input-wrapper" part="wrapper">
|
|
190
214
|
<div class="slot slot-start" part="slot-start">
|
|
@@ -205,10 +229,10 @@ var o, s = class extends t {
|
|
|
205
229
|
maxlength=${this.maxLength ?? ""}
|
|
206
230
|
minlength=${this.minLength ?? ""}
|
|
207
231
|
pattern=${this.pattern ?? ""}
|
|
208
|
-
@input=${this.#
|
|
209
|
-
@change=${this.#
|
|
210
|
-
@focus=${this.#
|
|
211
|
-
@blur=${this.#
|
|
232
|
+
@input=${this.#s}
|
|
233
|
+
@change=${this.#c}
|
|
234
|
+
@focus=${this.#l}
|
|
235
|
+
@blur=${this.#u}
|
|
212
236
|
/>
|
|
213
237
|
<div class="slot slot-end" part="slot-end">
|
|
214
238
|
<slot name="end"></slot>
|
|
@@ -216,10 +240,10 @@ var o, s = class extends t {
|
|
|
216
240
|
</div>
|
|
217
241
|
`;
|
|
218
242
|
}
|
|
219
|
-
#a;
|
|
220
|
-
#o;
|
|
221
243
|
#s;
|
|
222
244
|
#c;
|
|
245
|
+
#l;
|
|
246
|
+
#u;
|
|
223
247
|
};
|
|
224
248
|
e([a({ reflect: !0 })], s.prototype, "type", void 0), e([a({ reflect: !0 })], s.prototype, "size", void 0), e([a()], s.prototype, "value", void 0), e([a({ attribute: "default-value" })], s.prototype, "defaultValue", void 0), e([a()], s.prototype, "placeholder", void 0), e([a({
|
|
225
249
|
reflect: !0,
|
package/dist/sken-input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-input.js","names":["#internalValue","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenInputType, SkenInputSize } from '@sken-ds/contracts'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver(entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart = startWidth > 0\n ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n inputEl.style.paddingInlineEnd = endWidth > 0\n ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | null = null\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sken-input.js","names":["#internalValue","#syncFormValue","#internals","#slotStartObserver","#slotEndObserver","#visibilityObserver","#adjustPaddings","#handleInput","#handleChange","#handleFocus","#handleBlur"],"sources":["../src/components/sken-input.ts"],"sourcesContent":["// ── <sken-input> — framework-ready Web Component ─────────────────────\n// Wraps a native <input> with token-driven styles. Controlled and\n// uncontrolled value, every standard <input> type, free form\n// integration. Requires @sken-ds/theme/css (ADR-0006).\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenInputType, SkenInputSize } from '@sken-ds/contracts'\n\n@customElement('sken-input')\nexport class SkenInput extends LitElement {\n // Form-associated custom element: opt in to the ElementInternals\n // API so this primitive can participate in a surrounding\n // <form>'s submit/reset lifecycle. Without this flag, the\n // browser treats the host as an opaque element that does not\n // contribute to FormData and does not block Enter-to-submit\n // (the implicit submission rule for single text-like input\n // does not match against opaque elements). Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. We use it for:\n // - setFormValue() so the form picks up the current value.\n // - form reset (formResetCallback) for uncontrolled mode.\n // - reflecting `disabled` to the form's :disabled selector\n // (formDisabledCallback).\n // Nullable because some test environments (older happy-dom\n // versions, jsdom) do not implement ElementInternals. In\n // those environments form association is a no-op.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n // attachInternals() is only callable when formAssociated is\n // true. The static flag is set above, so this resolves to a\n // real ElementInternals in any spec-compliant browser\n // (Chrome 77+, Firefox 98+, Safari 16.4+). If the host\n // environment does not implement it, the call throws — we\n // catch and degrade gracefully so SSR / tests that do not\n // need form association still work.\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n @property({ reflect: true }) type: SkenInputType = 'text'\n @property({ reflect: true }) size: SkenInputSize = 'md'\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property() placeholder: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n @property() autocomplete: AutoFill | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ attribute: 'min-length', type: Number }) minLength: number | undefined = undefined\n @property() pattern: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from\n // `defaultValue` on first connection; from then on the DOM\n // input keeps the source of truth.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n block-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1; /* Firefox placeholder is <1 by default */\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: absolute so the slotted content floats over the\n input without consuming flex space. The input's padding is\n auto-adjusted in JS to keep the value text from overlapping.\n pointer-events: none on the wrapper, auto on the slotted\n content, so only the visible icon/button receives clicks. */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n min-block-size: 1.75rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n min-block-size: 2.75rem;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n input:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form so that\n // FormData(form) and the implicit-submit-on-Enter rule see\n // the primitive as a real text input from the very first\n // render. Cheap (no allocation in the hot path after this).\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotStartObserver?.disconnect()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────────\n // Called by the browser when the form this primitive is\n // associated with is reset (either via form.reset() or by\n // clicking a <button type=\"reset\">). For uncontrolled mode we\n // restore the seed value; for controlled mode the consumer's\n // prop is the source of truth, so we re-render and let\n // Lit's render path re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // Called by the browser when the form's :disabled state\n // changes (e.g. a fieldset ancestor is disabled, or the\n // primitive gets associated with a disabled form). Mirrors\n // the `disabled` state onto the inner <input> so the visual\n // and the form submission are consistent.\n formDisabledCallback(isDisabled: boolean): void {\n // Reflect onto the prop so Lit re-renders and the\n // :host([disabled]) CSS rule kicks in. The inner <input>\n // picks up `?disabled=${this.disabled}` in the template.\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form via ElementInternals.\n // Called on every change so FormData and submit/reset are\n // always in sync with the visible state.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n // If the primitive has no `name`, FormData(form) skips it\n // anyway, so setFormValue is harmless but pointless. We\n // still call it to keep the implicit-submit rule consistent\n // (the spec checks the existence of a single text-like\n // submitter, not the name).\n this.#internals.setFormValue(value)\n }\n\n protected override firstUpdated(): void {\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!startEl || !endEl || !inputEl) return\n\n // MutationObserver on each slot container watches for\n // children being added/removed/attributes-changed. Cheap\n // because the subtree is just one or two nodes.\n this.#slotStartObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotStartObserver.observe(startEl, { childList: true, subtree: true, attributes: true })\n this.#slotEndObserver = new MutationObserver(() => this.#adjustPaddings())\n this.#slotEndObserver.observe(endEl, { childList: true, subtree: true, attributes: true })\n\n // IntersectionObserver catches the case where the input is\n // hidden at first connect (e.g. inside a closed <details> or\n // an off-screen tab). The slot's bounding rect is 0 in that\n // case, so the padding would be wrong until the user opens\n // the parent. Re-measure when the host becomes visible.\n this.#visibilityObserver = new IntersectionObserver(entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustPaddings()\n }\n })\n this.#visibilityObserver.observe(this)\n\n // Also listen for the native slotchange event as a belt-and-\n // braces in case the MutationObserver misses something\n // (e.g. text node changes inside the slotted element).\n const startSlot = this.renderRoot.querySelector('slot[name=\"start\"]') as HTMLSlotElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n startSlot?.addEventListener('slotchange', this.#adjustPaddings)\n endSlot?.addEventListener('slotchange', this.#adjustPaddings)\n\n // First measurement.\n this.#adjustPaddings()\n }\n\n // The gap between a slot's edge and the input's text. iX uses\n // 0.5rem; we keep that because it balances the visual weight of\n // a 16px icon against the input's 0.75rem default padding-inline.\n // Module-scoped (not class-scoped) because TS forbids static\n // private fields on classes that use the legacy decorators.\n static SLOT_AIR = '0.5rem'\n\n // Two rAFs: the first lets Lit finish painting the slot, the\n // second lets the browser compute the slotted element's box.\n // Without the second, getBoundingClientRect() on a freshly-\n // assigned icon returns 0.\n #adjustPaddings = (): void => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (!inputEl) return\n const startEl = this.renderRoot.querySelector('.slot-start') as HTMLElement | null\n const endEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n // iX rule: padding = slot width + half a rem of air.\n // If the slot is empty, reset to the CSS default (no\n // inline style).\n const startWidth = startEl?.getBoundingClientRect().width ?? 0\n const endWidth = endEl?.getBoundingClientRect().width ?? 0\n inputEl.style.paddingInlineStart = startWidth > 0\n ? `calc(${startWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n inputEl.style.paddingInlineEnd = endWidth > 0\n ? `calc(${endWidth}px + ${SkenInput.SLOT_AIR})`\n : ''\n })\n })\n }\n\n // Observer handles. Nullable because they are not created until\n // firstUpdated (which only runs in the browser, not during\n // server-side rendering or unit tests that don't mount).\n #slotStartObserver: MutationObserver | null = null\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n .value=${value}\n type=${this.type}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n autocomplete=${this.autocomplete ?? 'off'}\n maxlength=${this.maxLength ?? ''}\n minlength=${this.minLength ?? ''}\n pattern=${this.pattern ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish to the surrounding form so the new value is\n // included in FormData and submit-on-Enter sees the latest\n // text. Per-keystroke is fine: setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n // Commit path: re-publish in case the browser mutated the\n // value (autofill, paste) without an intermediate input\n // event.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-input': SkenInput\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,12 @@ import { LitElement as t, css as n, html as r, nothing as i } from "lit";
|
|
|
3
3
|
import { customElement as a, property as o } from "lit/decorators.js";
|
|
4
4
|
//#region src/components/sken-number-input.ts
|
|
5
5
|
var s = 1, c = !0, l = class extends t {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
static {
|
|
7
|
+
this.formAssociated = !0;
|
|
8
|
+
}
|
|
9
|
+
#e;
|
|
10
|
+
constructor() {
|
|
11
|
+
super(), this.#e = null, this.defaultValue = void 0, this.value = void 0, this.min = void 0, this.max = void 0, this.step = s, this.showStepperButtons = c, this.placeholder = void 0, this.label = void 0, this.required = !1, this.disabled = !1, this.readonly = !1, this.helperText = void 0, this.infoText = void 0, this.warningText = void 0, this.validText = void 0, this.invalidText = void 0, this.textAlignment = "end", this.size = "md", this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = "0.5rem", this.#i = () => {
|
|
8
12
|
let e = this.renderRoot.querySelector("input"), t = this.renderRoot.querySelector(".steppers"), n = this.renderRoot.querySelector(".slot-end");
|
|
9
13
|
!e || !n || requestAnimationFrame(() => {
|
|
10
14
|
if (!e.isConnected) return;
|
|
@@ -13,59 +17,79 @@ var s = 1, c = !0, l = class extends t {
|
|
|
13
17
|
let c = s + i, l = Math.max(o, c) + a;
|
|
14
18
|
e.style.paddingInlineEnd = `${l}px`;
|
|
15
19
|
});
|
|
16
|
-
}, this.#
|
|
20
|
+
}, this.#a = null, this.#o = null, this.#c = (e) => {
|
|
17
21
|
let t = e.target;
|
|
18
|
-
this.#
|
|
19
|
-
let n = this.#
|
|
20
|
-
this.value, this.dispatchEvent(new CustomEvent("sken-input", {
|
|
22
|
+
this.#t = t.value;
|
|
23
|
+
let n = this.#v(t.value);
|
|
24
|
+
this.value, this.#n(), this.dispatchEvent(new CustomEvent("sken-input", {
|
|
21
25
|
detail: n,
|
|
22
26
|
bubbles: !0,
|
|
23
27
|
composed: !0
|
|
24
28
|
}));
|
|
25
|
-
}, this.#
|
|
26
|
-
let t = e.target, n = this.#
|
|
27
|
-
r !== n && (this.#
|
|
29
|
+
}, this.#l = (e) => {
|
|
30
|
+
let t = e.target, n = this.#v(t.value), r = this.#_(n);
|
|
31
|
+
r !== n && (this.#t = r === void 0 ? "" : String(r), t.value = this.#t), this.#n(), this.dispatchEvent(new CustomEvent("sken-change", {
|
|
28
32
|
detail: r,
|
|
29
33
|
bubbles: !0,
|
|
30
34
|
composed: !0
|
|
31
35
|
}));
|
|
32
|
-
}, this.#
|
|
36
|
+
}, this.#u = (e) => {
|
|
33
37
|
this.dispatchEvent(new CustomEvent("sken-focus", {
|
|
34
38
|
bubbles: !0,
|
|
35
39
|
composed: !0
|
|
36
40
|
}));
|
|
37
|
-
}, this.#
|
|
41
|
+
}, this.#d = (e) => {
|
|
38
42
|
this.dispatchEvent(new CustomEvent("sken-blur", {
|
|
39
43
|
bubbles: !0,
|
|
40
44
|
composed: !0
|
|
41
45
|
}));
|
|
42
|
-
}, this.#
|
|
46
|
+
}, this.#f = (e) => {
|
|
43
47
|
if (this.disabled || this.readonly) return;
|
|
44
48
|
let t = e.shiftKey || e.key === "PageUp" || e.key === "PageDown" ? 10 : 1;
|
|
45
|
-
e.key === "ArrowUp" ? (e.preventDefault(), this.#
|
|
46
|
-
}, this.#
|
|
49
|
+
e.key === "ArrowUp" ? (e.preventDefault(), this.#h(1, t)) : e.key === "ArrowDown" ? (e.preventDefault(), this.#h(-1, t)) : e.key === "PageUp" ? (e.preventDefault(), this.#h(1, 10)) : e.key === "PageDown" ? (e.preventDefault(), this.#h(-1, 10)) : e.key === "Home" && this.min !== void 0 ? (e.preventDefault(), this.#g(this.min)) : e.key === "End" && this.max !== void 0 ? (e.preventDefault(), this.#g(this.max)) : e.key;
|
|
50
|
+
}, this.#p = () => this.#h(1), this.#m = () => this.#h(-1);
|
|
51
|
+
try {
|
|
52
|
+
this.#e = this.attachInternals();
|
|
53
|
+
} catch {
|
|
54
|
+
this.#e = null;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
|
-
#
|
|
57
|
+
#t;
|
|
49
58
|
connectedCallback() {
|
|
50
|
-
super.connectedCallback(), this.#
|
|
59
|
+
super.connectedCallback(), this.#t = this.value === void 0 ? this.defaultValue === void 0 ? "" : String(this.defaultValue) : String(this.value), this.#n();
|
|
51
60
|
}
|
|
52
61
|
disconnectedCallback() {
|
|
53
|
-
super.disconnectedCallback(), this.#
|
|
62
|
+
super.disconnectedCallback(), this.#a?.disconnect(), this.#o?.disconnect();
|
|
63
|
+
}
|
|
64
|
+
formResetCallback() {
|
|
65
|
+
if (this.value === void 0) {
|
|
66
|
+
this.#t = this.defaultValue === void 0 ? "" : String(this.defaultValue);
|
|
67
|
+
let e = this.renderRoot.querySelector("input");
|
|
68
|
+
e && (e.value = this.#t), this.#n();
|
|
69
|
+
} else this.requestUpdate();
|
|
70
|
+
}
|
|
71
|
+
formDisabledCallback(e) {
|
|
72
|
+
this.disabled = e;
|
|
73
|
+
}
|
|
74
|
+
#n() {
|
|
75
|
+
if (!this.#e) return;
|
|
76
|
+
let e = this.value === void 0 ? "" : String(this.value);
|
|
77
|
+
this.#e.setFormValue(e);
|
|
54
78
|
}
|
|
55
79
|
firstUpdated() {
|
|
56
80
|
let e = this.renderRoot.querySelector(".slot-end"), t = this.renderRoot.querySelector("slot[name=\"end\"]");
|
|
57
|
-
e && t && (this.#
|
|
81
|
+
e && t && (this.#a = new MutationObserver(() => this.#i()), this.#a.observe(e, {
|
|
58
82
|
childList: !0,
|
|
59
83
|
subtree: !0,
|
|
60
84
|
attributes: !0
|
|
61
|
-
}), t.addEventListener("slotchange", this.#
|
|
62
|
-
for (let t of e) t.isIntersecting && this.#
|
|
63
|
-
}), this.#
|
|
85
|
+
}), t.addEventListener("slotchange", this.#i)), this.#o = new IntersectionObserver((e) => {
|
|
86
|
+
for (let t of e) t.isIntersecting && this.#i();
|
|
87
|
+
}), this.#o.observe(this), this.#i();
|
|
64
88
|
}
|
|
65
|
-
#t;
|
|
66
|
-
#n;
|
|
67
89
|
#r;
|
|
68
90
|
#i;
|
|
91
|
+
#a;
|
|
92
|
+
#o;
|
|
69
93
|
static {
|
|
70
94
|
this.styles = n`
|
|
71
95
|
:host {
|
|
@@ -368,7 +392,7 @@ var s = 1, c = !0, l = class extends t {
|
|
|
368
392
|
`;
|
|
369
393
|
}
|
|
370
394
|
render() {
|
|
371
|
-
let e = this.#
|
|
395
|
+
let e = this.#s();
|
|
372
396
|
return r`
|
|
373
397
|
<div class="field" part="field">
|
|
374
398
|
${this.label ? r`
|
|
@@ -383,7 +407,7 @@ var s = 1, c = !0, l = class extends t {
|
|
|
383
407
|
<input
|
|
384
408
|
part="input"
|
|
385
409
|
type="number"
|
|
386
|
-
.value=${this.#
|
|
410
|
+
.value=${this.#t}
|
|
387
411
|
placeholder=${this.placeholder ?? ""}
|
|
388
412
|
?disabled=${this.disabled}
|
|
389
413
|
?readonly=${this.readonly}
|
|
@@ -395,11 +419,11 @@ var s = 1, c = !0, l = class extends t {
|
|
|
395
419
|
aria-describedby=${this.describedById ?? ""}
|
|
396
420
|
name=${this.name ?? ""}
|
|
397
421
|
style="text-align: ${this.textAlignment}"
|
|
398
|
-
@input=${this.#
|
|
399
|
-
@change=${this.#
|
|
400
|
-
@focus=${this.#
|
|
401
|
-
@blur=${this.#
|
|
402
|
-
@keydown=${this.#
|
|
422
|
+
@input=${this.#c}
|
|
423
|
+
@change=${this.#l}
|
|
424
|
+
@focus=${this.#u}
|
|
425
|
+
@blur=${this.#d}
|
|
426
|
+
@keydown=${this.#f}
|
|
403
427
|
/>
|
|
404
428
|
<div class="slot slot-end" part="slot-end">
|
|
405
429
|
<slot name="end"></slot>
|
|
@@ -412,7 +436,7 @@ var s = 1, c = !0, l = class extends t {
|
|
|
412
436
|
type="button"
|
|
413
437
|
aria-label="Increment"
|
|
414
438
|
?disabled=${this.disabled || this.readonly || this.value !== void 0 && this.max !== void 0 && this.value >= this.max}
|
|
415
|
-
@click=${this.#
|
|
439
|
+
@click=${this.#p}
|
|
416
440
|
>
|
|
417
441
|
+
|
|
418
442
|
</button>
|
|
@@ -422,7 +446,7 @@ var s = 1, c = !0, l = class extends t {
|
|
|
422
446
|
type="button"
|
|
423
447
|
aria-label="Decrement"
|
|
424
448
|
?disabled=${this.disabled || this.readonly || this.value !== void 0 && this.min !== void 0 && this.value <= this.min}
|
|
425
|
-
@click=${this.#
|
|
449
|
+
@click=${this.#m}
|
|
426
450
|
>
|
|
427
451
|
−
|
|
428
452
|
</button>
|
|
@@ -435,7 +459,7 @@ var s = 1, c = !0, l = class extends t {
|
|
|
435
459
|
</div>
|
|
436
460
|
`;
|
|
437
461
|
}
|
|
438
|
-
#
|
|
462
|
+
#s() {
|
|
439
463
|
let e = [];
|
|
440
464
|
return this.invalidText && e.push(r`<p class="message" data-tone="invalid" part="message-invalid">
|
|
441
465
|
${this.invalidText}
|
|
@@ -449,25 +473,25 @@ var s = 1, c = !0, l = class extends t {
|
|
|
449
473
|
${this.helperText}
|
|
450
474
|
</p>`), e;
|
|
451
475
|
}
|
|
452
|
-
#o;
|
|
453
|
-
#s;
|
|
454
476
|
#c;
|
|
455
477
|
#l;
|
|
456
478
|
#u;
|
|
457
479
|
#d;
|
|
458
480
|
#f;
|
|
459
|
-
#p
|
|
481
|
+
#p;
|
|
482
|
+
#m;
|
|
483
|
+
#h(e, t = 1) {
|
|
460
484
|
if (this.disabled || this.readonly) return;
|
|
461
485
|
let n = this.renderRoot.querySelector("input")?.value ?? "", r = n === "" ? this.value ?? 0 : Number(n);
|
|
462
486
|
if (!Number.isFinite(r)) return;
|
|
463
487
|
let i = r + e * this.step * t;
|
|
464
|
-
this.#
|
|
488
|
+
this.#g(i);
|
|
465
489
|
}
|
|
466
|
-
#
|
|
467
|
-
let t = this.#
|
|
468
|
-
this.#
|
|
490
|
+
#g(e) {
|
|
491
|
+
let t = this.#_(e);
|
|
492
|
+
this.#t = t === void 0 ? "" : String(t);
|
|
469
493
|
let n = this.renderRoot.querySelector("input");
|
|
470
|
-
n && (n.value = this.#
|
|
494
|
+
n && (n.value = this.#t), this.#n(), this.dispatchEvent(new CustomEvent("sken-input", {
|
|
471
495
|
detail: t,
|
|
472
496
|
bubbles: !0,
|
|
473
497
|
composed: !0
|
|
@@ -477,12 +501,12 @@ var s = 1, c = !0, l = class extends t {
|
|
|
477
501
|
composed: !0
|
|
478
502
|
}));
|
|
479
503
|
}
|
|
480
|
-
#
|
|
504
|
+
#_(e) {
|
|
481
505
|
if (e === void 0 || Number.isNaN(e)) return;
|
|
482
506
|
let t = e;
|
|
483
507
|
return this.min !== void 0 && t < this.min && (t = this.min), this.max !== void 0 && t > this.max && (t = this.max), t;
|
|
484
508
|
}
|
|
485
|
-
#
|
|
509
|
+
#v(e) {
|
|
486
510
|
if (e === "" || e === "-") return;
|
|
487
511
|
let t = Number(e);
|
|
488
512
|
if (!Number.isNaN(t)) return t;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-number-input.js","names":["#rawText","#parseNumber","#clamp","#step","#commitValue","#slotEndObserver","#visibilityObserver","#adjustLayout","#renderMessages","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown","#handleStepUp","#handleStepDown"],"sources":["../src/components/sken-number-input.ts"],"sourcesContent":["// ── <sken-number-input> — Sken-owned Web Component ─────────────────\n// Lit 3 primitive for numeric input with visible ± stepper buttons,\n// min/max clamping, and the same start/end slot system as\n// <sken-input>. Promoted from the SkenNumberInput PoC story (which\n// was a thin wrap of <ix-number-input>). The PoC proved the UX;\n// this primitive owns the contract.\n\nimport { LitElement, html, css, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenNumberInputSize } from '@sken-ds/contracts'\n\n/**\n * Default step when the consumer does not pass one. Matches the\n * native `<input type=\"number\" step>` default.\n */\nconst DEFAULT_STEP = 1\n\n/**\n * Default value for `showStepperButtons`. The PoC validated that\n * the iX default of `false` is wrong for our use case: consumers\n * expect ± buttons for the numeric fields they use most (Alma\n * demo: quantity, temperature, score). The Sken primitive flips\n * the default to `true`; consumers who want a bare input pass\n * `showStepperButtons={false}` explicitly.\n */\nconst DEFAULT_SHOW_STEPPER_BUTTONS = true\n\n@customElement('sken-number-input')\nexport class SkenNumberInput extends LitElement {\n // ── Props ─────────────────────────────────────────────────────\n @property({ attribute: 'default-value', type: Number }) defaultValue: number | undefined = undefined\n @property({ type: Number }) value: number | undefined = undefined\n @property({ type: Number }) min: number | undefined = undefined\n @property({ type: Number }) max: number | undefined = undefined\n @property({ type: Number }) step: number = DEFAULT_STEP\n @property({ attribute: 'show-stepper-buttons', reflect: true, type: Boolean }) showStepperButtons =\n DEFAULT_SHOW_STEPPER_BUTTONS\n @property() placeholder: string | undefined = undefined\n @property() label: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ attribute: 'helper-text' }) helperText: string | undefined = undefined\n @property({ attribute: 'info-text' }) infoText: string | undefined = undefined\n @property({ attribute: 'warning-text' }) warningText: string | undefined = undefined\n @property({ attribute: 'valid-text' }) validText: string | undefined = undefined\n @property({ attribute: 'invalid-text' }) invalidText: string | undefined = undefined\n @property({ attribute: 'text-alignment' }) textAlignment: 'start' | 'end' = 'end'\n @property({ reflect: true }) size: SkenNumberInputSize = 'md'\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // ── Internal state ────────────────────────────────────────────\n /**\n * The raw text in the input. The DOM input is the source of\n * truth for the text (so the user can type a partial value like\n * \"-\", which is not a valid number yet). The numeric `value` is\n * derived from this on commit (blur, Enter, stepper).\n */\n #rawText: string = ''\n\n // ── DOM refs (queried on demand) ────────────────────────────\n\n // ── Lifecycle ─────────────────────────────────────────────────\n override connectedCallback(): void {\n super.connectedCallback()\n // Seed the raw text from the controlled value or the default.\n // We do NOT trigger a re-render here; the property is only\n // set after Lit has applied the host attributes, and the\n // first render will read the right value through this.value.\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n } else {\n this.#rawText = String(this.value)\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n /**\n * Set up the dynamic layout once the shadow root has the\n * element children we need to measure. Same pattern as\n * SkenInput: MutationObserver on the slot container to\n * catch children being added/removed, IntersectionObserver\n * to catch the case where the input is hidden at first\n * connect (e.g. inside a closed <details> or an off-screen\n * tab), and slotchange as belt-and-braces.\n */\n protected override firstUpdated(): void {\n const endContainer = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n if (endContainer && endSlot) {\n this.#slotEndObserver = new MutationObserver(() => this.#adjustLayout())\n this.#slotEndObserver.observe(endContainer, { childList: true, subtree: true, attributes: true })\n endSlot.addEventListener('slotchange', this.#adjustLayout)\n }\n this.#visibilityObserver = new IntersectionObserver(entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustLayout()\n }\n })\n this.#visibilityObserver.observe(this)\n this.#adjustLayout()\n }\n\n /**\n * The gap between a slot's edge and the input's text. 0.5rem\n * is what the SkenInput primitive uses (mirrors the iX rule).\n * The number input needs an extra \"air\" margin because the\n * stepper column is also on the right edge and we don't want\n * the value text to crash into the slot.\n */\n #SLOT_AIR = '0.5rem'\n\n /**\n * Measure the stepper column width and the end slot width, then\n * apply:\n * - The input's padding-inline-end so the value text never\n * overlaps the slot OR the stepper.\n * - The end slot's inset-inline-end so it sits to the LEFT of\n * the stepper (or at the right edge when no stepper).\n *\n * The math:\n * endInset = stepperReserve + air (slot's right edge)\n * paddingInline = max(endInset, stepperReserve) + air\n * If the end slot is empty, its width is 0 and the calc\n * falls back to the stepper-only reservation.\n */\n #adjustLayout = (): void => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const stepperEl = this.renderRoot.querySelector('.steppers') as HTMLElement | null\n const slotEndEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n if (!inputEl || !slotEndEl) return\n\n requestAnimationFrame(() => {\n if (!inputEl.isConnected) return\n const stepperWidth = stepperEl && this.showStepperButtons\n ? stepperEl.getBoundingClientRect().width\n : 0\n const endWidth = slotEndEl.getBoundingClientRect().width\n const air = parseFloat(getComputedStyle(inputEl).fontSize) * 0.5 // 0.5em in px\n const stepperReserve = stepperWidth\n // The slot's right edge sits at stepperReserve + air from\n // the input's right edge, so it never overlaps the stepper\n // column.\n const endInset = stepperReserve + air\n slotEndEl.style.insetInlineEnd = endInset > 0 ? `${endInset}px` : '0.5rem'\n // The input's padding-inline-end has to be at least the\n // wider of (stepperReserve, endInset + endWidth) so the\n // value text never overlaps either.\n const textReserve = endInset + endWidth\n const padEnd = Math.max(stepperReserve, textReserve) + air\n inputEl.style.paddingInlineEnd = `${padEnd}px`\n })\n }\n\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n // ── Styles ────────────────────────────────────────────────────\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .field {\n display: grid;\n gap: 0.375rem;\n }\n\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--sken-foreground);\n }\n\n .label[data-required='true']::after {\n content: ' *';\n color: var(--sken-destructive);\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n /* When the ± steppers are visible, reserve the right edge\n for them so the value text never overlaps. 1.5rem (button)\n + 2 * 0.25rem (insets) + 0.25rem (air) = 2.25rem. */\n padding-inline-end: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Hide the native steppers: we render our own. */\n -moz-appearance: textfield;\n }\n\n input::-webkit-outer-spin-button,\n input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Stepper buttons ───────────────────────────────────── */\n /*\n * The stepper column is a vertical pair of + and − buttons\n * anchored to the right edge of the input. Sizes scale with\n * the host's [size] attribute (sm / md / lg) so the buttons\n * match the input's vertical rhythm. Default (md) is 24x20\n * per button. Glyph color is --sken-foreground; hover swaps\n * to --sken-primary. Disabled state uses --sken-disabled.\n */\n .steppers {\n position: absolute;\n inset-block: 0.25rem;\n inset-inline-end: 0.25rem;\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n justify-content: center;\n }\n\n .stepper {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1.5rem;\n block-size: 1.25rem;\n background: transparent;\n border: 0;\n padding: 0;\n color: var(--sken-foreground);\n cursor: pointer;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1;\n font-weight: 600;\n border-radius: var(--sken-sm, 0.25rem);\n /* The parent .steppers is a flex column; prevent the\n buttons from being squashed when the host's vertical\n rhythm is tight (sm). The explicit block-size wins. */\n flex-shrink: 0;\n transition: color 120ms ease, background-color 120ms ease;\n }\n\n .stepper:hover:not(:disabled) {\n color: var(--sken-primary);\n /* No background fill on hover. A background would paint\n over the input's right border, hiding it. The color\n change to --sken-primary is enough affordance for a\n 20x24px icon button. */\n background: transparent;\n }\n\n .stepper:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 1px;\n }\n\n .stepper:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n /* When the ± steppers are visible, reserve the right edge\n of the input so the value text never overlaps. The\n reservation is 2rem for the md size (default): 1.5rem\n (button) + 2 * 0.25rem (column insets). The sm / lg\n variants below override the button size, so the\n reservation needs to scale too. */\n :host([show-stepper-buttons]) input {\n padding-inline-end: 2rem;\n }\n\n /* ── Stepper sizes ─────────────────────────────────────── */\n :host([size='sm']) .steppers {\n inset-block: 0.125rem;\n inset-inline-end: 0.125rem;\n gap: 1px;\n }\n :host([size='sm']) .stepper {\n inline-size: 1.125rem;\n block-size: 0.875rem;\n font-size: 0.75rem;\n }\n /* sm: 1.125rem (button) + 2 * 0.125rem (insets) = 1.375rem,\n plus 0.125rem of breathing air = 1.5rem. */\n :host([size='sm'][show-stepper-buttons]) input {\n padding-inline-end: 1.5rem;\n }\n\n :host([size='lg']) .steppers {\n inset-block: 0.375rem;\n inset-inline-end: 0.375rem;\n gap: 3px;\n }\n :host([size='lg']) .stepper {\n inline-size: 1.75rem;\n block-size: 1.5rem;\n font-size: 1.125rem;\n }\n /* lg: 1.75rem (button) + 2 * 0.375rem (insets) = 2.5rem,\n plus 0.25rem of breathing air = 2.75rem. */\n :host([size='lg'][show-stepper-buttons]) input {\n padding-inline-end: 2.75rem;\n }\n\n /* Hide steppers on touch devices where they would be hard\n to hit accurately. The user can still use ↑/↓ keys or\n type directly. */\n @media (hover: none) {\n .steppers {\n display: none;\n }\n input {\n padding-inline-end: var(--sken-3);\n }\n :host([size='sm']) input {\n padding-inline-end: var(--sken-2);\n }\n :host([size='lg']) input {\n padding-inline-end: var(--sken-4);\n }\n }\n\n /* ── Slot containers ────────────────────────────────────── */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n /*\n * The end slot's position is set in JS via the inline style\n * (see #adjustLayout). The rule below is a fallback for the\n * initial render before JS has measured the slot, and a\n * sensible default when the slot is empty.\n */\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── States ─────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n input:read-only {\n background: var(--sken-muted);\n }\n\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n\n /* ── Sizes ──────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n /* 2 × 0.875rem (buttons) + 1px (gap) + 2 × 0.125rem\n (insets) = ~2.0625rem. Round up to 2.25rem for visual\n breathing room. */\n min-block-size: 2.25rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n /* 2 × 1.5rem (buttons) + 3px (gap) + 2 × 0.375rem\n (insets) = ~3.9375rem. Round up to 4rem. */\n min-block-size: 4rem;\n }\n\n /* ── Validation messages ─────────────────────────────────── */\n .messages {\n display: grid;\n gap: 0.25rem;\n }\n\n .message {\n margin: 0;\n font-size: 0.75rem;\n line-height: 1.4;\n }\n\n .message[data-tone='info'] {\n color: var(--sken-info, #0082ff);\n }\n\n .message[data-tone='warning'] {\n color: var(--sken-warning, #f59e0b);\n }\n\n .message[data-tone='valid'] {\n color: var(--sken-success, #10b981);\n }\n\n .message[data-tone='invalid'] {\n color: var(--sken-destructive, #dc2626);\n }\n\n .message[data-tone='helper'] {\n color: var(--sken-muted-foreground);\n }\n `\n\n // ── Render ────────────────────────────────────────────────────\n protected override render() {\n const messages = this.#renderMessages()\n return html`\n <div class=\"field\" part=\"field\">\n ${this.label\n ? html`\n <label class=\"label\" part=\"label\" data-required=${this.required}>\n ${this.label}\n </label>\n `\n : nothing}\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n type=\"number\"\n .value=${this.#rawText}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n min=${this.min ?? ''}\n max=${this.max ?? ''}\n step=${this.step}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n style=\"text-align: ${this.textAlignment}\"\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n ${this.showStepperButtons\n ? html`\n <div class=\"steppers\" part=\"steppers\">\n <button\n class=\"stepper\"\n part=\"stepper stepper-up\"\n type=\"button\"\n aria-label=\"Increment\"\n ?disabled=${this.disabled || this.readonly ||\n (this.value !== undefined && this.max !== undefined && this.value >= this.max)}\n @click=${this.#handleStepUp}\n >\n +\n </button>\n <button\n class=\"stepper\"\n part=\"stepper stepper-down\"\n type=\"button\"\n aria-label=\"Decrement\"\n ?disabled=${this.disabled || this.readonly ||\n (this.value !== undefined && this.min !== undefined && this.value <= this.min)}\n @click=${this.#handleStepDown}\n >\n −\n </button>\n </div>\n `\n : nothing}\n </div>\n ${messages.length > 0\n ? html`\n <div class=\"messages\" part=\"messages\">${messages}</div>\n `\n : nothing}\n </div>\n `\n }\n\n #renderMessages() {\n const messages: ReturnType<typeof html>[] = []\n if (this.invalidText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"invalid\" part=\"message-invalid\">\n ${this.invalidText}\n </p>`,\n )\n }\n if (this.warningText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"warning\" part=\"message-warning\">\n ${this.warningText}\n </p>`,\n )\n }\n if (this.infoText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"info\" part=\"message-info\">\n ${this.infoText}\n </p>`,\n )\n }\n if (this.validText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"valid\" part=\"message-valid\">\n ${this.validText}\n </p>`,\n )\n }\n if (this.helperText && messages.length === 0) {\n messages.push(\n html`<p class=\"message\" data-tone=\"helper\" part=\"message-helper\">\n ${this.helperText}\n </p>`,\n )\n }\n return messages\n }\n\n // ── Event handlers ───────────────────────────────────────────\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n this.#rawText = target.value\n // Emit the parsed number (or undefined if empty / invalid).\n const parsed = this.#parseNumber(target.value)\n if (this.value === undefined) {\n // Uncontrolled: store internally, do not mutate this.value\n // because the contract is \"controlled by the consumer\".\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n } else {\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n }\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n const parsed = this.#parseNumber(target.value)\n // Clamp on commit. If the user typed 150 with max=100, we\n // commit 100 and update the input text.\n const clamped = this.#clamp(parsed)\n if (clamped !== parsed) {\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n target.value = this.#rawText\n }\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n #handleBlur = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-blur', { bubbles: true, composed: true }))\n }\n\n #handleKeydown = (event: KeyboardEvent) => {\n if (this.disabled || this.readonly) return\n // Shift+Arrow and PageUp/PageDown apply a 10x multiplier to\n // the step. This is the standard \"fast forward\" pattern in\n // numeric inputs across Mature DS (iX, Material, AntD). It\n // lets the consumer reach the target faster when the step\n // is small relative to the typical range.\n const multiplier = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n this.#step(+1, multiplier)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n this.#step(-1, multiplier)\n } else if (event.key === 'PageUp') {\n event.preventDefault()\n this.#step(+1, 10)\n } else if (event.key === 'PageDown') {\n event.preventDefault()\n this.#step(-1, 10)\n } else if (event.key === 'Home' && this.min !== undefined) {\n event.preventDefault()\n this.#commitValue(this.min)\n } else if (event.key === 'End' && this.max !== undefined) {\n event.preventDefault()\n this.#commitValue(this.max)\n } else if (event.key === 'Enter') {\n // Enter fires the change event naturally via the input;\n // no need to do anything here. The contract documents\n // \"Enter commits\" but the browser already does that.\n }\n }\n\n #handleStepUp = () => this.#step(+1)\n #handleStepDown = () => this.#step(-1)\n\n // ── Helpers ───────────────────────────────────────────────────\n /**\n * Increment / decrement the value by `step * multiplier`,\n * clamped at [min, max]. Emits `sken-input` and `sken-change`\n * (we treat the stepper as a commit, not a keystroke). The\n * `multiplier` defaults to 1; the keyboard handler passes 10\n * for Shift+Arrow and PageUp/PageDown (the \"fast forward\"\n * pattern).\n *\n * The current value is read from the rendered <input>, NOT\n * from `this.value`. Reading from the input is robust for\n * both modes:\n * - Controlled: the consumer is async (Vue's reactive update\n * applies on the next tick). By the time the second click\n * arrives, the consumer's `value` prop may still be the\n * pre-click value, but the input's `.value` is already\n * updated by #commitValue from the first click.\n * - Uncontrolled: the primitive owns the value. The input's\n * `.value` is the source of truth; `this.value` is the\n * initial seed and not maintained by the primitive in this\n * mode.\n */\n #step(direction: 1 | -1, multiplier: number = 1) {\n if (this.disabled || this.readonly) return\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const text = inputEl?.value ?? ''\n const current = text === '' ? (this.value ?? 0) : Number(text)\n if (!Number.isFinite(current)) return\n const next = current + direction * this.step * multiplier\n this.#commitValue(next)\n }\n\n /**\n * Commit a value: clamp, update internal state, emit events.\n * Same path used by the stepper buttons and the keyboard\n * Home / End shortcuts.\n */\n #commitValue(raw: number) {\n const clamped = this.#clamp(raw)\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n // Update the DOM input so the next @input sees the new value\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #clamp(value: number | undefined): number | undefined {\n if (value === undefined || Number.isNaN(value)) return undefined\n let v = value\n if (this.min !== undefined && v < this.min) v = this.min\n if (this.max !== undefined && v > this.max) v = this.max\n return v\n }\n\n #parseNumber(text: string): number | undefined {\n if (text === '' || text === '-') return undefined\n const n = Number(text)\n if (Number.isNaN(n)) return undefined\n return n\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-number-input': SkenNumberInput\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sken-number-input.js","names":["#rawText","#parseNumber","#syncFormValue","#clamp","#step","#commitValue","#internals","#slotEndObserver","#visibilityObserver","#adjustLayout","#renderMessages","#handleInput","#handleChange","#handleFocus","#handleBlur","#handleKeydown","#handleStepUp","#handleStepDown"],"sources":["../src/components/sken-number-input.ts"],"sourcesContent":["// ── <sken-number-input> — Sken-owned Web Component ─────────────────\n// Lit 3 primitive for numeric input with visible ± stepper buttons,\n// min/max clamping, and the same start/end slot system as\n// <sken-input>. Promoted from the SkenNumberInput PoC story (which\n// was a thin wrap of <ix-number-input>). The PoC proved the UX;\n// this primitive owns the contract.\n\nimport { LitElement, html, css, nothing } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type { SkenNumberInputSize } from '@sken-ds/contracts'\n\n/**\n * Default step when the consumer does not pass one. Matches the\n * native `<input type=\"number\" step>` default.\n */\nconst DEFAULT_STEP = 1\n\n/**\n * Default value for `showStepperButtons`. The PoC validated that\n * the iX default of `false` is wrong for our use case: consumers\n * expect ± buttons for the numeric fields they use most (Alma\n * demo: quantity, temperature, score). The Sken primitive flips\n * the default to `true`; consumers who want a bare input pass\n * `showStepperButtons={false}` explicitly.\n */\nconst DEFAULT_SHOW_STEPPER_BUTTONS = true\n\n@customElement('sken-number-input')\nexport class SkenNumberInput extends LitElement {\n // Form-associated custom element. Same pattern as SkenInput /\n // SkenTextarea. The numeric value is stringified for\n // FormData (FormData is text-only); undefined becomes the\n // empty string. The implicit-submit-on-Enter rule still\n // applies, so a form with a single number input and a\n // submit button will submit on Enter.\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // ElementInternals handle. Created in the constructor.\n // Nullable to gracefully degrade in test environments.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n // ── Props ─────────────────────────────────────────────────────\n @property({ attribute: 'default-value', type: Number }) defaultValue: number | undefined = undefined\n @property({ type: Number }) value: number | undefined = undefined\n @property({ type: Number }) min: number | undefined = undefined\n @property({ type: Number }) max: number | undefined = undefined\n @property({ type: Number }) step: number = DEFAULT_STEP\n @property({ attribute: 'show-stepper-buttons', reflect: true, type: Boolean }) showStepperButtons =\n DEFAULT_SHOW_STEPPER_BUTTONS\n @property() placeholder: string | undefined = undefined\n @property() label: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ attribute: 'helper-text' }) helperText: string | undefined = undefined\n @property({ attribute: 'info-text' }) infoText: string | undefined = undefined\n @property({ attribute: 'warning-text' }) warningText: string | undefined = undefined\n @property({ attribute: 'valid-text' }) validText: string | undefined = undefined\n @property({ attribute: 'invalid-text' }) invalidText: string | undefined = undefined\n @property({ attribute: 'text-alignment' }) textAlignment: 'start' | 'end' = 'end'\n @property({ reflect: true }) size: SkenNumberInputSize = 'md'\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // ── Internal state ────────────────────────────────────────────\n /**\n * The raw text in the input. The DOM input is the source of\n * truth for the text (so the user can type a partial value like\n * \"-\", which is not a valid number yet). The numeric `value` is\n * derived from this on commit (blur, Enter, stepper).\n */\n #rawText: string = ''\n\n // ── DOM refs (queried on demand) ────────────────────────────\n\n // ── Lifecycle ─────────────────────────────────────────────────\n override connectedCallback(): void {\n super.connectedCallback()\n // Seed the raw text from the controlled value or the default.\n // We do NOT trigger a re-render here; the property is only\n // set after Lit has applied the host attributes, and the\n // first render will read the right value through this.value.\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n } else {\n this.#rawText = String(this.value)\n }\n // Publish the initial value to the surrounding form.\n this.#syncFormValue()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.#slotEndObserver?.disconnect()\n this.#visibilityObserver?.disconnect()\n }\n\n // ── Form-association callbacks ─────────────────────────────\n // formResetCallback: restore uncontrolled seed; re-render\n // controlled so Lit re-publishes the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#rawText = this.defaultValue !== undefined ? String(this.defaultValue) : ''\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // formDisabledCallback: mirror the form's :disabled state\n // onto the prop. Lit re-renders; the inner <input> picks up\n // ?disabled=${this.disabled} in the template.\n formDisabledCallback(isDisabled: boolean): void {\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form. FormData is\n // text-only, so we stringify the number (or empty string\n // for undefined). The string roundtrip preserves precision\n // for integers and finite decimals; consumers that need\n // BigInt or arbitrary precision should re-parse on the\n // server side.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value !== undefined ? String(this.value) : ''\n this.#internals.setFormValue(value)\n }\n\n /**\n * Set up the dynamic layout once the shadow root has the\n * element children we need to measure. Same pattern as\n * SkenInput: MutationObserver on the slot container to\n * catch children being added/removed, IntersectionObserver\n * to catch the case where the input is hidden at first\n * connect (e.g. inside a closed <details> or an off-screen\n * tab), and slotchange as belt-and-braces.\n */\n protected override firstUpdated(): void {\n const endContainer = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n const endSlot = this.renderRoot.querySelector('slot[name=\"end\"]') as HTMLSlotElement | null\n if (endContainer && endSlot) {\n this.#slotEndObserver = new MutationObserver(() => this.#adjustLayout())\n this.#slotEndObserver.observe(endContainer, { childList: true, subtree: true, attributes: true })\n endSlot.addEventListener('slotchange', this.#adjustLayout)\n }\n this.#visibilityObserver = new IntersectionObserver(entries => {\n for (const entry of entries) {\n if (entry.isIntersecting) this.#adjustLayout()\n }\n })\n this.#visibilityObserver.observe(this)\n this.#adjustLayout()\n }\n\n /**\n * The gap between a slot's edge and the input's text. 0.5rem\n * is what the SkenInput primitive uses (mirrors the iX rule).\n * The number input needs an extra \"air\" margin because the\n * stepper column is also on the right edge and we don't want\n * the value text to crash into the slot.\n */\n #SLOT_AIR = '0.5rem'\n\n /**\n * Measure the stepper column width and the end slot width, then\n * apply:\n * - The input's padding-inline-end so the value text never\n * overlaps the slot OR the stepper.\n * - The end slot's inset-inline-end so it sits to the LEFT of\n * the stepper (or at the right edge when no stepper).\n *\n * The math:\n * endInset = stepperReserve + air (slot's right edge)\n * paddingInline = max(endInset, stepperReserve) + air\n * If the end slot is empty, its width is 0 and the calc\n * falls back to the stepper-only reservation.\n */\n #adjustLayout = (): void => {\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const stepperEl = this.renderRoot.querySelector('.steppers') as HTMLElement | null\n const slotEndEl = this.renderRoot.querySelector('.slot-end') as HTMLElement | null\n if (!inputEl || !slotEndEl) return\n\n requestAnimationFrame(() => {\n if (!inputEl.isConnected) return\n const stepperWidth = stepperEl && this.showStepperButtons\n ? stepperEl.getBoundingClientRect().width\n : 0\n const endWidth = slotEndEl.getBoundingClientRect().width\n const air = parseFloat(getComputedStyle(inputEl).fontSize) * 0.5 // 0.5em in px\n const stepperReserve = stepperWidth\n // The slot's right edge sits at stepperReserve + air from\n // the input's right edge, so it never overlaps the stepper\n // column.\n const endInset = stepperReserve + air\n slotEndEl.style.insetInlineEnd = endInset > 0 ? `${endInset}px` : '0.5rem'\n // The input's padding-inline-end has to be at least the\n // wider of (stepperReserve, endInset + endWidth) so the\n // value text never overlaps either.\n const textReserve = endInset + endWidth\n const padEnd = Math.max(stepperReserve, textReserve) + air\n inputEl.style.paddingInlineEnd = `${padEnd}px`\n })\n }\n\n #slotEndObserver: MutationObserver | null = null\n #visibilityObserver: IntersectionObserver | null = null\n\n // ── Styles ────────────────────────────────────────────────────\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .field {\n display: grid;\n gap: 0.375rem;\n }\n\n .label {\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--sken-foreground);\n }\n\n .label[data-required='true']::after {\n content: ' *';\n color: var(--sken-destructive);\n }\n\n .input-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n inline-size: 100%;\n }\n\n input {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.25;\n text-overflow: ellipsis;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n /* When the ± steppers are visible, reserve the right edge\n for them so the value text never overlaps. 1.5rem (button)\n + 2 * 0.25rem (insets) + 0.25rem (air) = 2.25rem. */\n padding-inline-end: var(--sken-3);\n min-block-size: 2.25rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Hide the native steppers: we render our own. */\n -moz-appearance: textfield;\n }\n\n input::-webkit-outer-spin-button,\n input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n input::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Stepper buttons ───────────────────────────────────── */\n /*\n * The stepper column is a vertical pair of + and − buttons\n * anchored to the right edge of the input. Sizes scale with\n * the host's [size] attribute (sm / md / lg) so the buttons\n * match the input's vertical rhythm. Default (md) is 24x20\n * per button. Glyph color is --sken-foreground; hover swaps\n * to --sken-primary. Disabled state uses --sken-disabled.\n */\n .steppers {\n position: absolute;\n inset-block: 0.25rem;\n inset-inline-end: 0.25rem;\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: center;\n justify-content: center;\n }\n\n .stepper {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: 1.5rem;\n block-size: 1.25rem;\n background: transparent;\n border: 0;\n padding: 0;\n color: var(--sken-foreground);\n cursor: pointer;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1;\n font-weight: 600;\n border-radius: var(--sken-sm, 0.25rem);\n /* The parent .steppers is a flex column; prevent the\n buttons from being squashed when the host's vertical\n rhythm is tight (sm). The explicit block-size wins. */\n flex-shrink: 0;\n transition: color 120ms ease, background-color 120ms ease;\n }\n\n .stepper:hover:not(:disabled) {\n color: var(--sken-primary);\n /* No background fill on hover. A background would paint\n over the input's right border, hiding it. The color\n change to --sken-primary is enough affordance for a\n 20x24px icon button. */\n background: transparent;\n }\n\n .stepper:focus-visible {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 1px;\n }\n\n .stepper:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n /* When the ± steppers are visible, reserve the right edge\n of the input so the value text never overlaps. The\n reservation is 2rem for the md size (default): 1.5rem\n (button) + 2 * 0.25rem (column insets). The sm / lg\n variants below override the button size, so the\n reservation needs to scale too. */\n :host([show-stepper-buttons]) input {\n padding-inline-end: 2rem;\n }\n\n /* ── Stepper sizes ─────────────────────────────────────── */\n :host([size='sm']) .steppers {\n inset-block: 0.125rem;\n inset-inline-end: 0.125rem;\n gap: 1px;\n }\n :host([size='sm']) .stepper {\n inline-size: 1.125rem;\n block-size: 0.875rem;\n font-size: 0.75rem;\n }\n /* sm: 1.125rem (button) + 2 * 0.125rem (insets) = 1.375rem,\n plus 0.125rem of breathing air = 1.5rem. */\n :host([size='sm'][show-stepper-buttons]) input {\n padding-inline-end: 1.5rem;\n }\n\n :host([size='lg']) .steppers {\n inset-block: 0.375rem;\n inset-inline-end: 0.375rem;\n gap: 3px;\n }\n :host([size='lg']) .stepper {\n inline-size: 1.75rem;\n block-size: 1.5rem;\n font-size: 1.125rem;\n }\n /* lg: 1.75rem (button) + 2 * 0.375rem (insets) = 2.5rem,\n plus 0.25rem of breathing air = 2.75rem. */\n :host([size='lg'][show-stepper-buttons]) input {\n padding-inline-end: 2.75rem;\n }\n\n /* Hide steppers on touch devices where they would be hard\n to hit accurately. The user can still use ↑/↓ keys or\n type directly. */\n @media (hover: none) {\n .steppers {\n display: none;\n }\n input {\n padding-inline-end: var(--sken-3);\n }\n :host([size='sm']) input {\n padding-inline-end: var(--sken-2);\n }\n :host([size='lg']) input {\n padding-inline-end: var(--sken-4);\n }\n }\n\n /* ── Slot containers ────────────────────────────────────── */\n .slot {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1;\n pointer-events: none;\n color: var(--sken-muted-foreground);\n }\n\n .slot ::slotted(*) {\n pointer-events: auto;\n }\n\n .slot-start {\n inset-inline-start: 0.5rem;\n }\n\n /*\n * The end slot's position is set in JS via the inline style\n * (see #adjustLayout). The rule below is a fallback for the\n * initial render before JS has measured the slot, and a\n * sensible default when the slot is empty.\n */\n .slot-end {\n inset-inline-end: 0.5rem;\n }\n\n /* ── States ─────────────────────────────────────────────── */\n .input-wrapper:hover input:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n\n .input-wrapper:focus-within input {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n\n input:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n\n input:read-only {\n background: var(--sken-muted);\n }\n\n :host([invalid]) input {\n border-color: var(--sken-destructive);\n }\n\n :host([invalid]) .input-wrapper:focus-within input {\n outline-color: var(--sken-destructive);\n }\n\n /* ── Sizes ──────────────────────────────────────────────── */\n :host([size='sm']) input {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n /* 2 × 0.875rem (buttons) + 1px (gap) + 2 × 0.125rem\n (insets) = ~2.0625rem. Round up to 2.25rem for visual\n breathing room. */\n min-block-size: 2.25rem;\n }\n :host([size='md']) input {\n /* Default styles above. */\n }\n :host([size='lg']) input {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n /* 2 × 1.5rem (buttons) + 3px (gap) + 2 × 0.375rem\n (insets) = ~3.9375rem. Round up to 4rem. */\n min-block-size: 4rem;\n }\n\n /* ── Validation messages ─────────────────────────────────── */\n .messages {\n display: grid;\n gap: 0.25rem;\n }\n\n .message {\n margin: 0;\n font-size: 0.75rem;\n line-height: 1.4;\n }\n\n .message[data-tone='info'] {\n color: var(--sken-info, #0082ff);\n }\n\n .message[data-tone='warning'] {\n color: var(--sken-warning, #f59e0b);\n }\n\n .message[data-tone='valid'] {\n color: var(--sken-success, #10b981);\n }\n\n .message[data-tone='invalid'] {\n color: var(--sken-destructive, #dc2626);\n }\n\n .message[data-tone='helper'] {\n color: var(--sken-muted-foreground);\n }\n `\n\n // ── Render ────────────────────────────────────────────────────\n protected override render() {\n const messages = this.#renderMessages()\n return html`\n <div class=\"field\" part=\"field\">\n ${this.label\n ? html`\n <label class=\"label\" part=\"label\" data-required=${this.required}>\n ${this.label}\n </label>\n `\n : nothing}\n <div class=\"input-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <input\n part=\"input\"\n type=\"number\"\n .value=${this.#rawText}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n min=${this.min ?? ''}\n max=${this.max ?? ''}\n step=${this.step}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n style=\"text-align: ${this.textAlignment}\"\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n @keydown=${this.#handleKeydown}\n />\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n ${this.showStepperButtons\n ? html`\n <div class=\"steppers\" part=\"steppers\">\n <button\n class=\"stepper\"\n part=\"stepper stepper-up\"\n type=\"button\"\n aria-label=\"Increment\"\n ?disabled=${this.disabled || this.readonly ||\n (this.value !== undefined && this.max !== undefined && this.value >= this.max)}\n @click=${this.#handleStepUp}\n >\n +\n </button>\n <button\n class=\"stepper\"\n part=\"stepper stepper-down\"\n type=\"button\"\n aria-label=\"Decrement\"\n ?disabled=${this.disabled || this.readonly ||\n (this.value !== undefined && this.min !== undefined && this.value <= this.min)}\n @click=${this.#handleStepDown}\n >\n −\n </button>\n </div>\n `\n : nothing}\n </div>\n ${messages.length > 0\n ? html`\n <div class=\"messages\" part=\"messages\">${messages}</div>\n `\n : nothing}\n </div>\n `\n }\n\n #renderMessages() {\n const messages: ReturnType<typeof html>[] = []\n if (this.invalidText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"invalid\" part=\"message-invalid\">\n ${this.invalidText}\n </p>`,\n )\n }\n if (this.warningText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"warning\" part=\"message-warning\">\n ${this.warningText}\n </p>`,\n )\n }\n if (this.infoText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"info\" part=\"message-info\">\n ${this.infoText}\n </p>`,\n )\n }\n if (this.validText) {\n messages.push(\n html`<p class=\"message\" data-tone=\"valid\" part=\"message-valid\">\n ${this.validText}\n </p>`,\n )\n }\n if (this.helperText && messages.length === 0) {\n messages.push(\n html`<p class=\"message\" data-tone=\"helper\" part=\"message-helper\">\n ${this.helperText}\n </p>`,\n )\n }\n return messages\n }\n\n // ── Event handlers ───────────────────────────────────────────\n #handleInput = (event: Event) => {\n const target = event.target as HTMLInputElement\n this.#rawText = target.value\n // Emit the parsed number (or undefined if empty / invalid).\n const parsed = this.#parseNumber(target.value)\n if (this.value === undefined) {\n // Uncontrolled: store internally, do not mutate this.value\n // because the contract is \"controlled by the consumer\".\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n } else {\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: parsed,\n bubbles: true,\n composed: true,\n }),\n )\n }\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLInputElement\n const parsed = this.#parseNumber(target.value)\n // Clamp on commit. If the user typed 150 with max=100, we\n // commit 100 and update the input text.\n const clamped = this.#clamp(parsed)\n if (clamped !== parsed) {\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n target.value = this.#rawText\n }\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-focus', { bubbles: true, composed: true }))\n }\n\n #handleBlur = (_event: FocusEvent) => {\n this.dispatchEvent(new CustomEvent('sken-blur', { bubbles: true, composed: true }))\n }\n\n #handleKeydown = (event: KeyboardEvent) => {\n if (this.disabled || this.readonly) return\n // Shift+Arrow and PageUp/PageDown apply a 10x multiplier to\n // the step. This is the standard \"fast forward\" pattern in\n // numeric inputs across Mature DS (iX, Material, AntD). It\n // lets the consumer reach the target faster when the step\n // is small relative to the typical range.\n const multiplier = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1\n if (event.key === 'ArrowUp') {\n event.preventDefault()\n this.#step(+1, multiplier)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n this.#step(-1, multiplier)\n } else if (event.key === 'PageUp') {\n event.preventDefault()\n this.#step(+1, 10)\n } else if (event.key === 'PageDown') {\n event.preventDefault()\n this.#step(-1, 10)\n } else if (event.key === 'Home' && this.min !== undefined) {\n event.preventDefault()\n this.#commitValue(this.min)\n } else if (event.key === 'End' && this.max !== undefined) {\n event.preventDefault()\n this.#commitValue(this.max)\n } else if (event.key === 'Enter') {\n // Enter fires the change event naturally via the input;\n // no need to do anything here. The contract documents\n // \"Enter commits\" but the browser already does that.\n }\n }\n\n #handleStepUp = () => this.#step(+1)\n #handleStepDown = () => this.#step(-1)\n\n // ── Helpers ───────────────────────────────────────────────────\n /**\n * Increment / decrement the value by `step * multiplier`,\n * clamped at [min, max]. Emits `sken-input` and `sken-change`\n * (we treat the stepper as a commit, not a keystroke). The\n * `multiplier` defaults to 1; the keyboard handler passes 10\n * for Shift+Arrow and PageUp/PageDown (the \"fast forward\"\n * pattern).\n *\n * The current value is read from the rendered <input>, NOT\n * from `this.value`. Reading from the input is robust for\n * both modes:\n * - Controlled: the consumer is async (Vue's reactive update\n * applies on the next tick). By the time the second click\n * arrives, the consumer's `value` prop may still be the\n * pre-click value, but the input's `.value` is already\n * updated by #commitValue from the first click.\n * - Uncontrolled: the primitive owns the value. The input's\n * `.value` is the source of truth; `this.value` is the\n * initial seed and not maintained by the primitive in this\n * mode.\n */\n #step(direction: 1 | -1, multiplier: number = 1) {\n if (this.disabled || this.readonly) return\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n const text = inputEl?.value ?? ''\n const current = text === '' ? (this.value ?? 0) : Number(text)\n if (!Number.isFinite(current)) return\n const next = current + direction * this.step * multiplier\n this.#commitValue(next)\n }\n\n /**\n * Commit a value: clamp, update internal state, emit events.\n * Same path used by the stepper buttons and the keyboard\n * Home / End shortcuts.\n */\n #commitValue(raw: number) {\n const clamped = this.#clamp(raw)\n this.#rawText = clamped !== undefined ? String(clamped) : ''\n // Update the DOM input so the next @input sees the new value\n const inputEl = this.renderRoot.querySelector('input') as HTMLInputElement | null\n if (inputEl) inputEl.value = this.#rawText\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-input', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n this.dispatchEvent(\n new CustomEvent<number | undefined>('sken-change', {\n detail: clamped,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #clamp(value: number | undefined): number | undefined {\n if (value === undefined || Number.isNaN(value)) return undefined\n let v = value\n if (this.min !== undefined && v < this.min) v = this.min\n if (this.max !== undefined && v > this.max) v = this.max\n return v\n }\n\n #parseNumber(text: string): number | undefined {\n if (text === '' || text === '-') return undefined\n const n = Number(text)\n if (Number.isNaN(n)) return undefined\n return n\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-number-input': SkenNumberInput\n }\n}\n"],"mappings":"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"}
|
package/dist/sken-textarea.js
CHANGED
|
@@ -3,36 +3,45 @@ import { LitElement as t, css as n, html as r } from "lit";
|
|
|
3
3
|
import { customElement as i, property as a } from "lit/decorators.js";
|
|
4
4
|
//#region src/components/sken-textarea.ts
|
|
5
5
|
var o = class extends t {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
static {
|
|
7
|
+
this.formAssociated = !0;
|
|
8
|
+
}
|
|
9
|
+
#e;
|
|
10
|
+
constructor() {
|
|
11
|
+
super(), this.#e = null, this.size = "md", this.rows = 4, this.cols = void 0, this.maxLength = void 0, this.resize = "vertical", this.placeholder = void 0, this.value = void 0, this.defaultValue = void 0, this.disabled = !1, this.readonly = !1, this.required = !1, this.invalid = !1, this.describedById = void 0, this.name = void 0, this.#t = "", this.#r = (e) => {
|
|
8
12
|
let t = e.target.value;
|
|
9
|
-
this.value === void 0 && (this.#
|
|
13
|
+
this.value === void 0 && (this.#t = t), this.#n(), this.dispatchEvent(new CustomEvent("sken-input", {
|
|
10
14
|
detail: t,
|
|
11
15
|
bubbles: !0,
|
|
12
16
|
composed: !0
|
|
13
17
|
}));
|
|
14
|
-
}, this.#
|
|
18
|
+
}, this.#i = (e) => {
|
|
15
19
|
let t = e.target;
|
|
16
|
-
this.dispatchEvent(new CustomEvent("sken-change", {
|
|
20
|
+
this.#n(), this.dispatchEvent(new CustomEvent("sken-change", {
|
|
17
21
|
detail: t.value,
|
|
18
22
|
bubbles: !0,
|
|
19
23
|
composed: !0
|
|
20
24
|
}));
|
|
21
|
-
}, this.#
|
|
25
|
+
}, this.#a = (e) => {
|
|
22
26
|
this.dispatchEvent(new CustomEvent("sken-focus", {
|
|
23
27
|
detail: e,
|
|
24
28
|
bubbles: !0,
|
|
25
29
|
composed: !0
|
|
26
30
|
}));
|
|
27
|
-
}, this.#
|
|
31
|
+
}, this.#o = (e) => {
|
|
28
32
|
this.dispatchEvent(new CustomEvent("sken-blur", {
|
|
29
33
|
detail: e,
|
|
30
34
|
bubbles: !0,
|
|
31
35
|
composed: !0
|
|
32
36
|
}));
|
|
33
37
|
};
|
|
38
|
+
try {
|
|
39
|
+
this.#e = this.attachInternals();
|
|
40
|
+
} catch {
|
|
41
|
+
this.#e = null;
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
|
-
#
|
|
44
|
+
#t;
|
|
36
45
|
static {
|
|
37
46
|
this.styles = n`
|
|
38
47
|
:host {
|
|
@@ -147,10 +156,25 @@ var o = class extends t {
|
|
|
147
156
|
`;
|
|
148
157
|
}
|
|
149
158
|
connectedCallback() {
|
|
150
|
-
super.connectedCallback(), this.value === void 0 && (this.#
|
|
159
|
+
super.connectedCallback(), this.value === void 0 && (this.#t = this.defaultValue ?? ""), this.#n();
|
|
160
|
+
}
|
|
161
|
+
formResetCallback() {
|
|
162
|
+
if (this.value === void 0) {
|
|
163
|
+
this.#t = this.defaultValue ?? "";
|
|
164
|
+
let e = this.renderRoot.querySelector("textarea");
|
|
165
|
+
e && (e.value = this.#t), this.#n();
|
|
166
|
+
} else this.requestUpdate();
|
|
167
|
+
}
|
|
168
|
+
formDisabledCallback(e) {
|
|
169
|
+
this.disabled = e;
|
|
170
|
+
}
|
|
171
|
+
#n() {
|
|
172
|
+
if (!this.#e) return;
|
|
173
|
+
let e = this.value ?? this.#t;
|
|
174
|
+
this.#e.setFormValue(e);
|
|
151
175
|
}
|
|
152
176
|
render() {
|
|
153
|
-
let e = this.value ?? this.#
|
|
177
|
+
let e = this.value ?? this.#t;
|
|
154
178
|
return r`
|
|
155
179
|
<div class="textarea-wrapper" part="wrapper">
|
|
156
180
|
<div class="slot slot-start" part="slot-start">
|
|
@@ -169,10 +193,10 @@ var o = class extends t {
|
|
|
169
193
|
aria-invalid=${this.invalid ? "true" : "false"}
|
|
170
194
|
aria-describedby=${this.describedById ?? ""}
|
|
171
195
|
name=${this.name ?? ""}
|
|
172
|
-
@input=${this.#
|
|
173
|
-
@change=${this.#
|
|
174
|
-
@focus=${this.#
|
|
175
|
-
@blur=${this.#
|
|
196
|
+
@input=${this.#r}
|
|
197
|
+
@change=${this.#i}
|
|
198
|
+
@focus=${this.#a}
|
|
199
|
+
@blur=${this.#o}
|
|
176
200
|
></textarea>
|
|
177
201
|
<div class="slot slot-end" part="slot-end">
|
|
178
202
|
<slot name="end"></slot>
|
|
@@ -180,10 +204,10 @@ var o = class extends t {
|
|
|
180
204
|
</div>
|
|
181
205
|
`;
|
|
182
206
|
}
|
|
183
|
-
#t;
|
|
184
|
-
#n;
|
|
185
207
|
#r;
|
|
186
208
|
#i;
|
|
209
|
+
#a;
|
|
210
|
+
#o;
|
|
187
211
|
};
|
|
188
212
|
e([a({ reflect: !0 })], o.prototype, "size", void 0), e([a({
|
|
189
213
|
reflect: !0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sken-textarea.js","names":["#internalValue","#handleInput","#handleChange","#handleFocus","#handleBlur"],"sources":["../src/components/sken-textarea.ts"],"sourcesContent":["// ── <sken-textarea> — framework-ready Web Component ────────────────\n// Multiline text input. Wraps a native <textarea> with token-driven\n// styles and the same shadow-DOM event surface as <sken-input>.\n// Slots are placed above (start) and below (end) the textarea, not\n// to the sides — multiline editors don't lend themselves to\n// horizontal adornments the way single-line inputs do.\n//\n// Requires @sken-ds/theme/css (ADR-0006). Pairs with @sken-ds/contracts'\n// SkenTextareaProps / SkenTextareaSlots / SkenTextareaEmits.\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type {\n SkenTextareaSize,\n SkenTextareaResize,\n} from '@sken-ds/contracts'\n\n@customElement('sken-textarea')\nexport class SkenTextarea extends LitElement {\n @property({ reflect: true }) size: SkenTextareaSize = 'md'\n @property({ reflect: true, type: Number }) rows = 4\n @property({ reflect: true, type: Number }) cols: number | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ reflect: true }) resize: SkenTextareaResize = 'vertical'\n @property() placeholder: string | undefined = undefined\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from `defaultValue`\n // on first connection; from then on the DOM textarea keeps the\n // source of truth. Same pattern as SkenInput.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .textarea-wrapper {\n position: relative;\n display: flex;\n flex-direction: column;\n inline-size: 100%;\n }\n\n textarea {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.5;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 4.5rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Resize is controlled by the resize attribute, not by\n user-agent stylesheet, so consumers can override. */\n }\n\n textarea::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: relative (not absolute) so the slotted content\n flows naturally above and below the textarea. The wrapper\n has a flex column, so the textarea's vertical padding is\n NOT auto-adjusted (unlike SkenInput). Slot height adds to\n the wrapper's natural height. */\n .slot {\n display: flex;\n align-items: center;\n color: var(--sken-muted-foreground);\n min-block-size: 0.25rem;\n }\n\n .slot-start {\n padding-block-end: 0.25rem;\n }\n\n .slot-end {\n padding-block-start: 0.25rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) textarea {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n }\n :host([size='md']) textarea {\n /* Default styles above. */\n }\n :host([size='lg']) textarea {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n }\n\n /* ── Resize policies ────────────────────────────────────── */\n :host([resize='none']) textarea {\n resize: none;\n }\n :host([resize='vertical']) textarea {\n resize: vertical;\n }\n :host([resize='horizontal']) textarea {\n resize: horizontal;\n }\n :host([resize='both']) textarea {\n resize: both;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .textarea-wrapper:hover textarea:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .textarea-wrapper:focus-within textarea {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n textarea:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n textarea:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) textarea {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .textarea-wrapper:focus-within textarea {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n }\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"textarea-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <textarea\n part=\"textarea\"\n .value=${value}\n rows=${this.rows}\n cols=${this.cols ?? ''}\n maxlength=${this.maxLength ?? ''}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n ></textarea>\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen. Mirrors SkenInput.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLTextAreaElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLTextAreaElement\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-textarea': SkenTextarea\n }\n}\n"],"mappings":";;;;AAkBO,IAAM,IAAN,cAA2B,EAAW;;EAwM5B,aAvMuC,KAAA,OAAA,MACJ,KAAA,OAAA,GACoB,KAAA,OAAA,KAAA,GACe,KAAA,YAAA,KAAA,GAC3B,KAAA,SAAA,YACZ,KAAA,cAAA,KAAA,GACN,KAAA,QAAA,KAAA,GACqC,KAAA,eAAA,KAAA,GACtB,KAAA,WAAA,IACA,KAAA,WAAA,IACA,KAAA,WAAA,IACD,KAAA,UAAA,IAC0B,KAAA,gBAAA,KAAA,GACzC,KAAA,OAAA,KAAA,GAKd,KAAA,KAAA,IA2JT,KAAA,MAAA,MAAiB;GAE/B,IAAM,IADS,EAAM,OACG;GAExB,AADI,KAAK,UAAU,KAAA,MAAW,KAAKA,KAAiB,IACpD,KAAK,cACH,IAAI,YAAoB,cAAc;IAAE,QAAQ;IAAU,SAAS;IAAM,UAAU;GAAK,CAAC,CAC3F;EACF,GAEiB,KAAA,MAAA,MAAiB;GAChC,IAAM,IAAS,EAAM;GACrB,KAAK,cACH,IAAI,YAAoB,eAAe;IACrC,QAAQ,EAAO;IACf,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF,GAEgB,KAAA,MAAA,MAAsB;GACpC,KAAK,cACH,IAAI,YAAwB,cAAc;IAAE,QAAQ;IAAO,SAAS;IAAM,UAAU;GAAK,CAAC,CAC5F;EACF,GAEe,KAAA,MAAA,MAAsB;GACnC,KAAK,cACH,IAAI,YAAwB,aAAa;IAAE,QAAQ;IAAO,SAAS;IAAM,UAAU;GAAK,CAAC,CAC3F;EACF;;CAzLA;;EAEgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgHnB,oBAAmC;EAEjC,AADA,MAAM,kBAAkB,GACpB,KAAK,UAAU,KAAA,MACjB,KAAKA,KAAiB,KAAK,gBAAgB;CAE/C;CAEA,SAA4B;EAC1B,IAAM,IAAQ,KAAK,SAAS,KAAKA;EACjC,OAAO,CAAI;;;;;;;mBAOI,EAAM;iBACR,KAAK,KAAK;iBACV,KAAK,QAAQ,GAAG;sBACX,KAAK,aAAa,GAAG;wBACnB,KAAK,eAAe,GAAG;sBACzB,KAAK,SAAS;sBACd,KAAK,SAAS;sBACd,KAAK,SAAS;yBACX,KAAK,UAAU,SAAS,QAAQ;6BAC5B,KAAK,iBAAiB,GAAG;iBACrC,KAAK,QAAQ,GAAG;mBACd,KAAKC,GAAa;oBACjB,KAAKC,GAAc;mBACpB,KAAKC,GAAa;kBACnB,KAAKC,GAAY;;;;;;;CAOjC;CAIA;CASA;CAWA;CAMA;AAKF;AA5MG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,WAAW;CAAc,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GAClD,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACT,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACT,EAAA,CAAA,EAAS,EAAE,WAAW,gBAAgB,CAAC,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GACvC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,EAAE,WAAW,kBAAkB,CAAC,CAAA,GAAA,EAAA,WAAA,iBAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAfX,IAAA,EAAA,CAAA,EAAc,eAAe,CAAA,GAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"sken-textarea.js","names":["#internalValue","#syncFormValue","#internals","#handleInput","#handleChange","#handleFocus","#handleBlur"],"sources":["../src/components/sken-textarea.ts"],"sourcesContent":["// ── <sken-textarea> — framework-ready Web Component ────────────────\n// Multiline text input. Wraps a native <textarea> with token-driven\n// styles and the same shadow-DOM event surface as <sken-input>.\n// Slots are placed above (start) and below (end) the textarea, not\n// to the sides — multiline editors don't lend themselves to\n// horizontal adornments the way single-line inputs do.\n//\n// Requires @sken-ds/theme/css (ADR-0006). Pairs with @sken-ds/contracts'\n// SkenTextareaProps / SkenTextareaSlots / SkenTextareaEmits.\n\nimport { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport type {\n SkenTextareaSize,\n SkenTextareaResize,\n} from '@sken-ds/contracts'\n\n@customElement('sken-textarea')\nexport class SkenTextarea extends LitElement {\n // Form-associated custom element. Opt in to the\n // ElementInternals API so this primitive participates in\n // the surrounding <form>'s submit/reset lifecycle and\n // FormData(form) picks up the textarea's value under its\n // `name` attribute. See SkenInput for the full rationale;\n // the pattern is identical. Lit docs:\n // https://lit.dev/docs/components/form/\n static formAssociated = true\n\n // The ElementInternals instance. Created in the constructor\n // via attachInternals(), which the browser only makes\n // available because `formAssociated` is true. Nullable to\n // gracefully degrade in test environments that do not\n // implement ElementInternals.\n #internals: ElementInternals | null = null\n\n constructor() {\n super()\n try {\n this.#internals = this.attachInternals()\n } catch {\n this.#internals = null\n }\n }\n\n @property({ reflect: true }) size: SkenTextareaSize = 'md'\n @property({ reflect: true, type: Number }) rows = 4\n @property({ reflect: true, type: Number }) cols: number | undefined = undefined\n @property({ attribute: 'max-length', type: Number }) maxLength: number | undefined = undefined\n @property({ reflect: true }) resize: SkenTextareaResize = 'vertical'\n @property() placeholder: string | undefined = undefined\n @property() value: string | undefined = undefined\n @property({ attribute: 'default-value' }) defaultValue: string | undefined = undefined\n @property({ reflect: true, type: Boolean }) disabled = false\n @property({ reflect: true, type: Boolean }) readonly = false\n @property({ reflect: true, type: Boolean }) required = false\n @property({ reflect: true, type: Boolean }) invalid = false\n @property({ attribute: 'described-by-id' }) describedById: string | undefined = undefined\n @property() name: string | undefined = undefined\n\n // Internal value for uncontrolled mode. Seeded from `defaultValue`\n // on first connection; from then on the DOM textarea keeps the\n // source of truth. Same pattern as SkenInput.\n #internalValue: string = ''\n\n static styles = css`\n :host {\n display: inline-block;\n inline-size: 100%;\n }\n\n .textarea-wrapper {\n position: relative;\n display: flex;\n flex-direction: column;\n inline-size: 100%;\n }\n\n textarea {\n box-sizing: border-box;\n inline-size: 100%;\n border: 1px solid var(--sken-border);\n border-radius: var(--sken-md);\n background: var(--sken-input);\n color: var(--sken-foreground);\n font-family: var(--sken-family-sans);\n font-size: var(--sken-size-md);\n line-height: 1.5;\n padding-block: var(--sken-2);\n padding-inline: var(--sken-3);\n min-block-size: 4.5rem;\n transition:\n border-color 120ms ease,\n box-shadow 120ms ease;\n /* Resize is controlled by the resize attribute, not by\n user-agent stylesheet, so consumers can override. */\n }\n\n textarea::placeholder {\n color: var(--sken-muted-foreground);\n opacity: 1;\n }\n\n /* ── Slot containers ──────────────────────────────────────\n Position: relative (not absolute) so the slotted content\n flows naturally above and below the textarea. The wrapper\n has a flex column, so the textarea's vertical padding is\n NOT auto-adjusted (unlike SkenInput). Slot height adds to\n the wrapper's natural height. */\n .slot {\n display: flex;\n align-items: center;\n color: var(--sken-muted-foreground);\n min-block-size: 0.25rem;\n }\n\n .slot-start {\n padding-block-end: 0.25rem;\n }\n\n .slot-end {\n padding-block-start: 0.25rem;\n }\n\n /* ── Sizes ────────────────────────────────────────────────── */\n :host([size='sm']) textarea {\n padding-block: var(--sken-1);\n padding-inline: var(--sken-2);\n font-size: var(--sken-size-sm);\n }\n :host([size='md']) textarea {\n /* Default styles above. */\n }\n :host([size='lg']) textarea {\n padding-block: var(--sken-3);\n padding-inline: var(--sken-4);\n font-size: var(--sken-size-lg);\n }\n\n /* ── Resize policies ────────────────────────────────────── */\n :host([resize='none']) textarea {\n resize: none;\n }\n :host([resize='vertical']) textarea {\n resize: vertical;\n }\n :host([resize='horizontal']) textarea {\n resize: horizontal;\n }\n :host([resize='both']) textarea {\n resize: both;\n }\n\n /* ── States ───────────────────────────────────────────────── */\n .textarea-wrapper:hover textarea:not(:disabled):not(:read-only) {\n border-color: var(--sken-foreground);\n }\n .textarea-wrapper:focus-within textarea {\n outline: 2px solid var(--sken-ring, currentColor);\n outline-offset: 2px;\n border-color: var(--sken-primary);\n }\n textarea:disabled {\n cursor: not-allowed;\n opacity: var(--sken-disabled);\n }\n textarea:read-only {\n background: var(--sken-muted);\n }\n :host([invalid]) textarea {\n border-color: var(--sken-destructive);\n }\n :host([invalid]) .textarea-wrapper:focus-within textarea {\n outline-color: var(--sken-destructive);\n }\n `\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n }\n // Publish the initial value to the surrounding form.\n this.#syncFormValue()\n }\n\n // ── Form-association callbacks ─────────────────────────────\n // formResetCallback: for uncontrolled mode, restore the seed\n // value and re-publish. For controlled mode, re-render and\n // let Lit re-publish the controlled value.\n formResetCallback(): void {\n if (this.value === undefined) {\n this.#internalValue = this.defaultValue ?? ''\n const ta = this.renderRoot.querySelector('textarea') as HTMLTextAreaElement | null\n if (ta) ta.value = this.#internalValue\n this.#syncFormValue()\n } else {\n this.requestUpdate()\n }\n }\n\n // formDisabledCallback: mirror the form's :disabled state onto\n // the prop. Lit re-renders; the inner <textarea> picks up\n // `?disabled=${this.disabled}` in the template.\n formDisabledCallback(isDisabled: boolean): void {\n this.disabled = isDisabled\n }\n\n // Publish the current value to the form. Called on every\n // change so FormData and submit/reset stay in sync.\n #syncFormValue(): void {\n if (!this.#internals) return\n const value = this.value ?? this.#internalValue\n this.#internals.setFormValue(value)\n }\n\n protected override render() {\n const value = this.value ?? this.#internalValue\n return html`\n <div class=\"textarea-wrapper\" part=\"wrapper\">\n <div class=\"slot slot-start\" part=\"slot-start\">\n <slot name=\"start\"></slot>\n </div>\n <textarea\n part=\"textarea\"\n .value=${value}\n rows=${this.rows}\n cols=${this.cols ?? ''}\n maxlength=${this.maxLength ?? ''}\n placeholder=${this.placeholder ?? ''}\n ?disabled=${this.disabled}\n ?readonly=${this.readonly}\n ?required=${this.required}\n aria-invalid=${this.invalid ? 'true' : 'false'}\n aria-describedby=${this.describedById ?? ''}\n name=${this.name ?? ''}\n @input=${this.#handleInput}\n @change=${this.#handleChange}\n @focus=${this.#handleFocus}\n @blur=${this.#handleBlur}\n ></textarea>\n <div class=\"slot slot-end\" part=\"slot-end\">\n <slot name=\"end\"></slot>\n </div>\n </div>\n `\n }\n\n // composed: true on every event so the framework adapter outside\n // the shadow boundary can listen. Mirrors SkenInput.\n #handleInput = (event: Event) => {\n const target = event.target as HTMLTextAreaElement\n const newValue = target.value\n if (this.value === undefined) this.#internalValue = newValue\n // Publish the new value to the surrounding form so submit\n // and FormData see the latest text. Per-keystroke is fine:\n // setFormValue is cheap.\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-input', { detail: newValue, bubbles: true, composed: true }),\n )\n }\n\n #handleChange = (event: Event) => {\n const target = event.target as HTMLTextAreaElement\n this.#syncFormValue()\n this.dispatchEvent(\n new CustomEvent<string>('sken-change', {\n detail: target.value,\n bubbles: true,\n composed: true,\n }),\n )\n }\n\n #handleFocus = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-focus', { detail: event, bubbles: true, composed: true }),\n )\n }\n\n #handleBlur = (event: FocusEvent) => {\n this.dispatchEvent(\n new CustomEvent<FocusEvent>('sken-blur', { detail: event, bubbles: true, composed: true }),\n )\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'sken-textarea': SkenTextarea\n }\n}\n"],"mappings":";;;;AAkBO,IAAM,IAAN,cAA2B,EAAW;;EAQnB,KAAA,iBAAA;;CAOxB;CAEA,cAAc;EAqPC,AApPb,MAAM,GAH8B,KAAA,KAAA,MAWgB,KAAA,OAAA,MACJ,KAAA,OAAA,GACoB,KAAA,OAAA,KAAA,GACe,KAAA,YAAA,KAAA,GAC3B,KAAA,SAAA,YACZ,KAAA,cAAA,KAAA,GACN,KAAA,QAAA,KAAA,GACqC,KAAA,eAAA,KAAA,GACtB,KAAA,WAAA,IACA,KAAA,WAAA,IACA,KAAA,WAAA,IACD,KAAA,UAAA,IAC0B,KAAA,gBAAA,KAAA,GACzC,KAAA,OAAA,KAAA,GAKd,KAAA,KAAA,IA2LT,KAAA,MAAA,MAAiB;GAE/B,IAAM,IADS,EAAM,OACG;GAMxB,AALI,KAAK,UAAU,KAAA,MAAW,KAAKA,KAAiB,IAIpD,KAAKC,GAAe,GACpB,KAAK,cACH,IAAI,YAAoB,cAAc;IAAE,QAAQ;IAAU,SAAS;IAAM,UAAU;GAAK,CAAC,CAC3F;EACF,GAEiB,KAAA,MAAA,MAAiB;GAChC,IAAM,IAAS,EAAM;GAErB,AADA,KAAKA,GAAe,GACpB,KAAK,cACH,IAAI,YAAoB,eAAe;IACrC,QAAQ,EAAO;IACf,SAAS;IACT,UAAU;GACZ,CAAC,CACH;EACF,GAEgB,KAAA,MAAA,MAAsB;GACpC,KAAK,cACH,IAAI,YAAwB,cAAc;IAAE,QAAQ;IAAO,SAAS;IAAM,UAAU;GAAK,CAAC,CAC5F;EACF,GAEe,KAAA,MAAA,MAAsB;GACnC,KAAK,cACH,IAAI,YAAwB,aAAa;IAAE,QAAQ;IAAO,SAAS;IAAM,UAAU;GAAK,CAAC,CAC3F;EACF;EAvPE,IAAI;GACF,KAAKC,KAAa,KAAK,gBAAgB;EACzC,QAAQ;GACN,KAAKA,KAAa;EACpB;CACF;CAoBA;;EAEgB,KAAA,SAAA,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgHnB,oBAAmC;EAMjC,AALA,MAAM,kBAAkB,GACpB,KAAK,UAAU,KAAA,MACjB,KAAKF,KAAiB,KAAK,gBAAgB,KAG7C,KAAKC,GAAe;CACtB;CAMA,oBAA0B;EACxB,IAAI,KAAK,UAAU,KAAA,GAAW;GAC5B,KAAKD,KAAiB,KAAK,gBAAgB;GAC3C,IAAM,IAAK,KAAK,WAAW,cAAc,UAAU;GAEnD,AADI,MAAI,EAAG,QAAQ,KAAKA,KACxB,KAAKC,GAAe;EACtB,OACE,KAAK,cAAc;CAEvB;CAKA,qBAAqB,GAA2B;EAC9C,KAAK,WAAW;CAClB;CAIA,KAAuB;EACrB,IAAI,CAAC,KAAKC,IAAY;EACtB,IAAM,IAAQ,KAAK,SAAS,KAAKF;EACjC,KAAKE,GAAW,aAAa,CAAK;CACpC;CAEA,SAA4B;EAC1B,IAAM,IAAQ,KAAK,SAAS,KAAKF;EACjC,OAAO,CAAI;;;;;;;mBAOI,EAAM;iBACR,KAAK,KAAK;iBACV,KAAK,QAAQ,GAAG;sBACX,KAAK,aAAa,GAAG;wBACnB,KAAK,eAAe,GAAG;sBACzB,KAAK,SAAS;sBACd,KAAK,SAAS;sBACd,KAAK,SAAS;yBACX,KAAK,UAAU,SAAS,QAAQ;6BAC5B,KAAK,iBAAiB,GAAG;iBACrC,KAAK,QAAQ,GAAG;mBACd,KAAKG,GAAa;oBACjB,KAAKC,GAAc;mBACpB,KAAKC,GAAa;kBACnB,KAAKC,GAAY;;;;;;;CAOjC;CAIA;CAaA;CAYA;CAMA;AAKF;AAjPG,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,WAAW;CAAc,MAAM;AAAO,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GAClD,EAAA,CAAA,EAAS,EAAE,SAAS,GAAK,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAC1B,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACT,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACT,EAAA,CAAA,EAAS,EAAE,WAAW,gBAAgB,CAAC,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GACvC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,SAAS;CAAM,MAAM;AAAQ,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,EAAE,WAAW,kBAAkB,CAAC,CAAA,GAAA,EAAA,WAAA,iBAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAxCX,IAAA,EAAA,CAAA,EAAc,eAAe,CAAA,GAAA,CAAA"}
|