@mfp-design-system/input 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +27 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +246 -0
- package/dist/input.js.map +1 -0
- package/package.json +51 -0
- package/src/index.ts +2 -0
- package/src/input.stories.ts +164 -0
- package/src/input.ts +242 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Melissa Pula
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @mfp-design-system/input
|
|
2
|
+
|
|
3
|
+
A Lit-based `<mfp-input>` web component. Works in any framework that supports custom elements.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @mfp-design-system/input @mfp-design-system/tokens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@mfp-design-system/tokens` is an optional peer — the component has fallback values, but loading the tokens gives it the canonical look.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import '@mfp-design-system/input';
|
|
17
|
+
import '@mfp-design-system/tokens/css';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<mfp-input label="Email" type="email" placeholder="you@example.com"></mfp-input>
|
|
22
|
+
|
|
23
|
+
<mfp-input label="Password" type="password" required></mfp-input>
|
|
24
|
+
|
|
25
|
+
<mfp-input label="Search" type="search">
|
|
26
|
+
<span slot="prefix">🔍</span>
|
|
27
|
+
</mfp-input>
|
|
28
|
+
|
|
29
|
+
<mfp-input label="Name" hint="Your full legal name"></mfp-input>
|
|
30
|
+
|
|
31
|
+
<mfp-input label="Email" type="email" error="Please enter a valid email address"></mfp-input>
|
|
32
|
+
|
|
33
|
+
<mfp-input size="sm" placeholder="Small"></mfp-input>
|
|
34
|
+
<mfp-input size="md" placeholder="Medium"></mfp-input>
|
|
35
|
+
<mfp-input size="lg" placeholder="Large"></mfp-input>
|
|
36
|
+
|
|
37
|
+
<mfp-input disabled value="Can't touch this"></mfp-input>
|
|
38
|
+
<mfp-input readonly value="Look but don't touch"></mfp-input>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
| Attribute | Type | Default | Description |
|
|
44
|
+
| ------------- | --------------------------------------------------------------------------- | -------- | -------------------------------------------- |
|
|
45
|
+
| `type` | `'text' \| 'email' \| 'password' \| 'number' \| 'search' \| 'tel' \| 'url'` | `'text'` | Native input type |
|
|
46
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing |
|
|
47
|
+
| `value` | `string` | `''` | Current value (controlled) |
|
|
48
|
+
| `name` | `string` | `''` | Native form `name` |
|
|
49
|
+
| `label` | `string` | `''` | Visible label (also wires `for`/`id`) |
|
|
50
|
+
| `placeholder` | `string` | `''` | Placeholder text |
|
|
51
|
+
| `hint` | `string` | `''` | Helper text shown below the input |
|
|
52
|
+
| `error` | `string` | `''` | Error message — also triggers invalid styles |
|
|
53
|
+
| `disabled` | `boolean` | `false` | Disables the input |
|
|
54
|
+
| `readonly` | `boolean` | `false` | Makes the input read-only |
|
|
55
|
+
| `required` | `boolean` | `false` | Marks as required (adds \* to label) |
|
|
56
|
+
|
|
57
|
+
### Events
|
|
58
|
+
|
|
59
|
+
- `input` — fires on every keystroke. `event.detail.value` carries the current value.
|
|
60
|
+
- `change` — fires on commit (blur or Enter), per native `<input>` semantics.
|
|
61
|
+
|
|
62
|
+
### Slots
|
|
63
|
+
|
|
64
|
+
- `prefix` — content before the input (icons, currency symbols, etc.)
|
|
65
|
+
- `suffix` — content after the input (clear button, units, etc.)
|
|
66
|
+
|
|
67
|
+
### Shadow parts
|
|
68
|
+
|
|
69
|
+
For custom styling: `label`, `control`, `input`, `hint`, `error`.
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
mfp-input::part(input) {
|
|
73
|
+
border-radius: 0;
|
|
74
|
+
}
|
|
75
|
+
mfp-input::part(label) {
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Framework notes
|
|
81
|
+
|
|
82
|
+
- **Vue 3 / Nuxt**: set `compilerOptions.isCustomElement: (tag) => tag.startsWith('mfp-')`
|
|
83
|
+
- **Angular**: add `CUSTOM_ELEMENTS_SCHEMA` to modules using the component
|
|
84
|
+
- **React 19+**: works natively
|
|
85
|
+
|
|
86
|
+
## Known limitations
|
|
87
|
+
|
|
88
|
+
- Does not yet participate in HTML form submission (no `ElementInternals` form-association). Listen for `input`/`change` events instead. On the roadmap, will land at the same time as Button gets it.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
export type InputType = 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
|
|
4
|
+
export declare class MfpInput extends LitElement {
|
|
5
|
+
static styles: import("lit").CSSResult;
|
|
6
|
+
size: InputSize;
|
|
7
|
+
type: InputType;
|
|
8
|
+
value: string;
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
hint: string;
|
|
13
|
+
error: string;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
readonly: boolean;
|
|
16
|
+
required: boolean;
|
|
17
|
+
private _id;
|
|
18
|
+
private _onInput;
|
|
19
|
+
private _onChange;
|
|
20
|
+
render(): import("lit").TemplateResult<1>;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
interface HTMLElementTagNameMap {
|
|
24
|
+
'mfp-input': MfpInput;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAGrD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC3C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAC;AAI5F,qBACa,QAAS,SAAQ,UAAU;IACpC,OAAgB,MAAM,0BA0HpB;IAGF,IAAI,EAAE,SAAS,CAAQ;IAGvB,IAAI,EAAE,SAAS,CAAU;IAGzB,KAAK,SAAM;IAGX,IAAI,SAAM;IAGV,KAAK,SAAM;IAGX,WAAW,SAAM;IAGjB,IAAI,SAAM;IAGV,KAAK,SAAM;IAGX,QAAQ,UAAS;IAGjB,QAAQ,UAAS;IAGjB,QAAQ,UAAS;IAEjB,OAAO,CAAC,GAAG,CAAmC;IAE9C,OAAO,CAAC,QAAQ,CAUd;IAEF,OAAO,CAAC,SAAS,CAUf;IAEO,MAAM;CA0ClB;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
9
|
+
let inputIdCounter = 0;
|
|
10
|
+
let MfpInput = class MfpInput extends LitElement {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.size = 'md';
|
|
14
|
+
this.type = 'text';
|
|
15
|
+
this.value = '';
|
|
16
|
+
this.name = '';
|
|
17
|
+
this.label = '';
|
|
18
|
+
this.placeholder = '';
|
|
19
|
+
this.hint = '';
|
|
20
|
+
this.error = '';
|
|
21
|
+
this.disabled = false;
|
|
22
|
+
this.readonly = false;
|
|
23
|
+
this.required = false;
|
|
24
|
+
this._id = `mfp-input-${++inputIdCounter}`;
|
|
25
|
+
this._onInput = (e) => {
|
|
26
|
+
const input = e.target;
|
|
27
|
+
this.value = input.value;
|
|
28
|
+
this.dispatchEvent(new CustomEvent('input', {
|
|
29
|
+
bubbles: true,
|
|
30
|
+
composed: true,
|
|
31
|
+
detail: { value: input.value },
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
this._onChange = (e) => {
|
|
35
|
+
const input = e.target;
|
|
36
|
+
this.value = input.value;
|
|
37
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
38
|
+
bubbles: true,
|
|
39
|
+
composed: true,
|
|
40
|
+
detail: { value: input.value },
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
static { this.styles = css `
|
|
45
|
+
:host {
|
|
46
|
+
display: block;
|
|
47
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
48
|
+
color: var(--color-text-default, #111827);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:host([disabled]) {
|
|
52
|
+
opacity: 0.6;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
label {
|
|
56
|
+
display: block;
|
|
57
|
+
font-size: var(--font-size-sm, 14px);
|
|
58
|
+
font-weight: var(--font-weight-medium, 500);
|
|
59
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
60
|
+
margin-bottom: var(--size-spacing-2, 8px);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.required {
|
|
64
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
65
|
+
margin-left: var(--size-spacing-1, 4px);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.control {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--size-spacing-2, 8px);
|
|
72
|
+
background: var(--color-neutral-0, #ffffff);
|
|
73
|
+
border: 1px solid var(--color-border-default, #e5e7eb);
|
|
74
|
+
border-radius: var(--size-radius-md, 8px);
|
|
75
|
+
transition:
|
|
76
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
77
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.control:focus-within {
|
|
81
|
+
border-color: var(--color-status-info-solid, #2563eb);
|
|
82
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.control.invalid {
|
|
86
|
+
border-color: var(--color-status-error-solid, #dc2626);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.control.invalid:focus-within {
|
|
90
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
::slotted([slot='prefix']),
|
|
94
|
+
::slotted([slot='suffix']) {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
color: var(--color-text-muted, #6b7280);
|
|
98
|
+
flex: none;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
::slotted([slot='prefix']) {
|
|
102
|
+
padding-left: var(--size-spacing-3, 12px);
|
|
103
|
+
}
|
|
104
|
+
::slotted([slot='suffix']) {
|
|
105
|
+
padding-right: var(--size-spacing-3, 12px);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input {
|
|
109
|
+
flex: 1 1 auto;
|
|
110
|
+
min-width: 0;
|
|
111
|
+
background: transparent;
|
|
112
|
+
border: none;
|
|
113
|
+
outline: none;
|
|
114
|
+
font: inherit;
|
|
115
|
+
color: inherit;
|
|
116
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-3, 12px);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
input::placeholder {
|
|
120
|
+
color: var(--color-text-muted, #6b7280);
|
|
121
|
+
opacity: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
input:disabled,
|
|
125
|
+
input:read-only {
|
|
126
|
+
cursor: not-allowed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
130
|
+
:host(:not([size])) input,
|
|
131
|
+
:host([size='md']) input {
|
|
132
|
+
font-size: var(--font-size-base, 16px);
|
|
133
|
+
min-height: 40px;
|
|
134
|
+
}
|
|
135
|
+
:host([size='sm']) input {
|
|
136
|
+
font-size: var(--font-size-sm, 14px);
|
|
137
|
+
min-height: 32px;
|
|
138
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
139
|
+
}
|
|
140
|
+
:host([size='lg']) input {
|
|
141
|
+
font-size: var(--font-size-lg, 18px);
|
|
142
|
+
min-height: 48px;
|
|
143
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-4, 16px);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.hint,
|
|
147
|
+
.error {
|
|
148
|
+
margin: var(--size-spacing-2, 8px) 0 0;
|
|
149
|
+
font-size: var(--font-size-sm, 14px);
|
|
150
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hint {
|
|
154
|
+
color: var(--color-text-muted, #6b7280);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.error {
|
|
158
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
.control {
|
|
163
|
+
transition: none;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
`; }
|
|
167
|
+
render() {
|
|
168
|
+
const invalid = this.error.length > 0;
|
|
169
|
+
const inputId = this._id;
|
|
170
|
+
const hintId = `${inputId}-hint`;
|
|
171
|
+
const errorId = `${inputId}-error`;
|
|
172
|
+
const describedBy = invalid ? errorId : this.hint ? hintId : undefined;
|
|
173
|
+
return html `
|
|
174
|
+
${this.label
|
|
175
|
+
? html `<label part="label" for=${inputId}>
|
|
176
|
+
${this.label}
|
|
177
|
+
${this.required
|
|
178
|
+
? html `<span class="required" aria-hidden="true">*</span>`
|
|
179
|
+
: nothing}
|
|
180
|
+
</label>`
|
|
181
|
+
: nothing}
|
|
182
|
+
<div part="control" class="control ${invalid ? 'invalid' : ''}">
|
|
183
|
+
<slot name="prefix"></slot>
|
|
184
|
+
<input
|
|
185
|
+
id=${inputId}
|
|
186
|
+
part="input"
|
|
187
|
+
type=${this.type}
|
|
188
|
+
.value=${this.value}
|
|
189
|
+
name=${this.name}
|
|
190
|
+
placeholder=${this.placeholder}
|
|
191
|
+
?disabled=${this.disabled}
|
|
192
|
+
?readonly=${this.readonly}
|
|
193
|
+
?required=${this.required}
|
|
194
|
+
aria-invalid=${invalid ? 'true' : 'false'}
|
|
195
|
+
aria-describedby=${describedBy ?? nothing}
|
|
196
|
+
@input=${this._onInput}
|
|
197
|
+
@change=${this._onChange}
|
|
198
|
+
/>
|
|
199
|
+
<slot name="suffix"></slot>
|
|
200
|
+
</div>
|
|
201
|
+
${invalid
|
|
202
|
+
? html `<p part="error" id=${errorId} class="error" role="alert">${this.error}</p>`
|
|
203
|
+
: this.hint
|
|
204
|
+
? html `<p part="hint" id=${hintId} class="hint">${this.hint}</p>`
|
|
205
|
+
: nothing}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
__decorate([
|
|
210
|
+
property({ reflect: true })
|
|
211
|
+
], MfpInput.prototype, "size", void 0);
|
|
212
|
+
__decorate([
|
|
213
|
+
property()
|
|
214
|
+
], MfpInput.prototype, "type", void 0);
|
|
215
|
+
__decorate([
|
|
216
|
+
property()
|
|
217
|
+
], MfpInput.prototype, "value", void 0);
|
|
218
|
+
__decorate([
|
|
219
|
+
property()
|
|
220
|
+
], MfpInput.prototype, "name", void 0);
|
|
221
|
+
__decorate([
|
|
222
|
+
property()
|
|
223
|
+
], MfpInput.prototype, "label", void 0);
|
|
224
|
+
__decorate([
|
|
225
|
+
property()
|
|
226
|
+
], MfpInput.prototype, "placeholder", void 0);
|
|
227
|
+
__decorate([
|
|
228
|
+
property()
|
|
229
|
+
], MfpInput.prototype, "hint", void 0);
|
|
230
|
+
__decorate([
|
|
231
|
+
property()
|
|
232
|
+
], MfpInput.prototype, "error", void 0);
|
|
233
|
+
__decorate([
|
|
234
|
+
property({ type: Boolean, reflect: true })
|
|
235
|
+
], MfpInput.prototype, "disabled", void 0);
|
|
236
|
+
__decorate([
|
|
237
|
+
property({ type: Boolean, reflect: true })
|
|
238
|
+
], MfpInput.prototype, "readonly", void 0);
|
|
239
|
+
__decorate([
|
|
240
|
+
property({ type: Boolean, reflect: true })
|
|
241
|
+
], MfpInput.prototype, "required", void 0);
|
|
242
|
+
MfpInput = __decorate([
|
|
243
|
+
customElement('mfp-input')
|
|
244
|
+
], MfpInput);
|
|
245
|
+
export { MfpInput };
|
|
246
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mfp-design-system/input",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Input web component for the mfp-design-system, built with Lit.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": true,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"customElements": "./dist/custom-elements.json",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"lit": "^3.2.1"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@mfp-design-system/tokens": "^0.2.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@mfp-design-system/tokens": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"rimraf": "^6.0.1",
|
|
37
|
+
"typescript": "^5.6.3",
|
|
38
|
+
"@mfp-design-system/tokens": "0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
46
|
+
"clean": "rimraf dist",
|
|
47
|
+
"lint": "eslint . --max-warnings=0",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "echo \"no tests yet\""
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import './input.js';
|
|
4
|
+
import type { InputSize, InputType } from './input.js';
|
|
5
|
+
|
|
6
|
+
interface Args {
|
|
7
|
+
label: string;
|
|
8
|
+
type: InputType;
|
|
9
|
+
size: InputSize;
|
|
10
|
+
placeholder: string;
|
|
11
|
+
value: string;
|
|
12
|
+
hint: string;
|
|
13
|
+
error: string;
|
|
14
|
+
disabled: boolean;
|
|
15
|
+
readonly: boolean;
|
|
16
|
+
required: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const meta: Meta<Args> = {
|
|
20
|
+
title: 'Components/Input',
|
|
21
|
+
component: 'mfp-input',
|
|
22
|
+
tags: ['autodocs'],
|
|
23
|
+
argTypes: {
|
|
24
|
+
label: { control: 'text' },
|
|
25
|
+
type: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url'],
|
|
28
|
+
},
|
|
29
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
30
|
+
placeholder: { control: 'text' },
|
|
31
|
+
value: { control: 'text' },
|
|
32
|
+
hint: { control: 'text' },
|
|
33
|
+
error: { control: 'text' },
|
|
34
|
+
disabled: { control: 'boolean' },
|
|
35
|
+
readonly: { control: 'boolean' },
|
|
36
|
+
required: { control: 'boolean' },
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
label: 'Email',
|
|
40
|
+
type: 'email',
|
|
41
|
+
size: 'md',
|
|
42
|
+
placeholder: 'you@example.com',
|
|
43
|
+
value: '',
|
|
44
|
+
hint: '',
|
|
45
|
+
error: '',
|
|
46
|
+
disabled: false,
|
|
47
|
+
readonly: false,
|
|
48
|
+
required: false,
|
|
49
|
+
},
|
|
50
|
+
render: (args) => html`
|
|
51
|
+
<mfp-input
|
|
52
|
+
label=${args.label}
|
|
53
|
+
type=${args.type}
|
|
54
|
+
size=${args.size}
|
|
55
|
+
placeholder=${args.placeholder}
|
|
56
|
+
.value=${args.value}
|
|
57
|
+
hint=${args.hint}
|
|
58
|
+
error=${args.error}
|
|
59
|
+
?disabled=${args.disabled}
|
|
60
|
+
?readonly=${args.readonly}
|
|
61
|
+
?required=${args.required}
|
|
62
|
+
></mfp-input>
|
|
63
|
+
`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default meta;
|
|
67
|
+
|
|
68
|
+
type Story = StoryObj<Args>;
|
|
69
|
+
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
args: { label: '', placeholder: 'Type here…' },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const WithLabel: Story = {
|
|
75
|
+
args: { label: 'Email', placeholder: 'you@example.com' },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const WithHint: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
label: 'Username',
|
|
81
|
+
placeholder: 'mfreundschuh',
|
|
82
|
+
hint: '3–20 characters, letters and numbers only.',
|
|
83
|
+
type: 'text',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const WithError: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
label: 'Email',
|
|
90
|
+
placeholder: 'you@example.com',
|
|
91
|
+
value: 'not-an-email',
|
|
92
|
+
error: 'Please enter a valid email address.',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const Required: Story = {
|
|
97
|
+
args: { label: 'Full name', placeholder: 'Melissa Pula', required: true, type: 'text' },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Sizes: Story = {
|
|
101
|
+
parameters: { controls: { disable: true } },
|
|
102
|
+
render: () => html`
|
|
103
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
|
|
104
|
+
<mfp-input size="sm" label="Small" placeholder="sm"></mfp-input>
|
|
105
|
+
<mfp-input size="md" label="Medium" placeholder="md"></mfp-input>
|
|
106
|
+
<mfp-input size="lg" label="Large" placeholder="lg"></mfp-input>
|
|
107
|
+
</div>
|
|
108
|
+
`,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const Types: Story = {
|
|
112
|
+
parameters: { controls: { disable: true } },
|
|
113
|
+
render: () => html`
|
|
114
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
|
|
115
|
+
<mfp-input type="text" label="Text" placeholder="Plain text"></mfp-input>
|
|
116
|
+
<mfp-input type="email" label="Email" placeholder="you@example.com"></mfp-input>
|
|
117
|
+
<mfp-input type="password" label="Password" placeholder="••••••••"></mfp-input>
|
|
118
|
+
<mfp-input type="number" label="Number" placeholder="42"></mfp-input>
|
|
119
|
+
<mfp-input type="search" label="Search" placeholder="Search…"></mfp-input>
|
|
120
|
+
<mfp-input type="tel" label="Phone" placeholder="(555) 555-5555"></mfp-input>
|
|
121
|
+
<mfp-input type="url" label="URL" placeholder="https://example.com"></mfp-input>
|
|
122
|
+
</div>
|
|
123
|
+
`,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const States: Story = {
|
|
127
|
+
parameters: { controls: { disable: true } },
|
|
128
|
+
render: () => html`
|
|
129
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
|
|
130
|
+
<mfp-input label="Normal" placeholder="editable"></mfp-input>
|
|
131
|
+
<mfp-input label="Disabled" .value=${"can't touch this"} disabled></mfp-input>
|
|
132
|
+
<mfp-input
|
|
133
|
+
label="Read-only"
|
|
134
|
+
.value=${'look but don’t touch'}
|
|
135
|
+
readonly
|
|
136
|
+
></mfp-input>
|
|
137
|
+
<mfp-input label="Required" placeholder="must fill" required></mfp-input>
|
|
138
|
+
<mfp-input
|
|
139
|
+
label="Invalid"
|
|
140
|
+
.value=${'oops'}
|
|
141
|
+
error="Something is wrong here."
|
|
142
|
+
></mfp-input>
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const WithPrefix: Story = {
|
|
148
|
+
parameters: { controls: { disable: true } },
|
|
149
|
+
render: () => html`
|
|
150
|
+
<mfp-input label="Search" type="search" placeholder="Search…" style="max-width: 320px;">
|
|
151
|
+
<span slot="prefix">🔍</span>
|
|
152
|
+
</mfp-input>
|
|
153
|
+
`,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const WithSuffix: Story = {
|
|
157
|
+
parameters: { controls: { disable: true } },
|
|
158
|
+
render: () => html`
|
|
159
|
+
<mfp-input label="Amount" type="number" placeholder="0.00" style="max-width: 320px;">
|
|
160
|
+
<span slot="prefix">$</span>
|
|
161
|
+
<span slot="suffix">USD</span>
|
|
162
|
+
</mfp-input>
|
|
163
|
+
`,
|
|
164
|
+
};
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
export type InputType = 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
|
|
6
|
+
|
|
7
|
+
let inputIdCounter = 0;
|
|
8
|
+
|
|
9
|
+
@customElement('mfp-input')
|
|
10
|
+
export class MfpInput extends LitElement {
|
|
11
|
+
static override styles = css`
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
15
|
+
color: var(--color-text-default, #111827);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host([disabled]) {
|
|
19
|
+
opacity: 0.6;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
label {
|
|
23
|
+
display: block;
|
|
24
|
+
font-size: var(--font-size-sm, 14px);
|
|
25
|
+
font-weight: var(--font-weight-medium, 500);
|
|
26
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
27
|
+
margin-bottom: var(--size-spacing-2, 8px);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.required {
|
|
31
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
32
|
+
margin-left: var(--size-spacing-1, 4px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.control {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--size-spacing-2, 8px);
|
|
39
|
+
background: var(--color-neutral-0, #ffffff);
|
|
40
|
+
border: 1px solid var(--color-border-default, #e5e7eb);
|
|
41
|
+
border-radius: var(--size-radius-md, 8px);
|
|
42
|
+
transition:
|
|
43
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
44
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.control:focus-within {
|
|
48
|
+
border-color: var(--color-status-info-solid, #2563eb);
|
|
49
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.control.invalid {
|
|
53
|
+
border-color: var(--color-status-error-solid, #dc2626);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.control.invalid:focus-within {
|
|
57
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
::slotted([slot='prefix']),
|
|
61
|
+
::slotted([slot='suffix']) {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
color: var(--color-text-muted, #6b7280);
|
|
65
|
+
flex: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
::slotted([slot='prefix']) {
|
|
69
|
+
padding-left: var(--size-spacing-3, 12px);
|
|
70
|
+
}
|
|
71
|
+
::slotted([slot='suffix']) {
|
|
72
|
+
padding-right: var(--size-spacing-3, 12px);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
input {
|
|
76
|
+
flex: 1 1 auto;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
background: transparent;
|
|
79
|
+
border: none;
|
|
80
|
+
outline: none;
|
|
81
|
+
font: inherit;
|
|
82
|
+
color: inherit;
|
|
83
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-3, 12px);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
input::placeholder {
|
|
87
|
+
color: var(--color-text-muted, #6b7280);
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
input:disabled,
|
|
92
|
+
input:read-only {
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
97
|
+
:host(:not([size])) input,
|
|
98
|
+
:host([size='md']) input {
|
|
99
|
+
font-size: var(--font-size-base, 16px);
|
|
100
|
+
min-height: 40px;
|
|
101
|
+
}
|
|
102
|
+
:host([size='sm']) input {
|
|
103
|
+
font-size: var(--font-size-sm, 14px);
|
|
104
|
+
min-height: 32px;
|
|
105
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
106
|
+
}
|
|
107
|
+
:host([size='lg']) input {
|
|
108
|
+
font-size: var(--font-size-lg, 18px);
|
|
109
|
+
min-height: 48px;
|
|
110
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-4, 16px);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.hint,
|
|
114
|
+
.error {
|
|
115
|
+
margin: var(--size-spacing-2, 8px) 0 0;
|
|
116
|
+
font-size: var(--font-size-sm, 14px);
|
|
117
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.hint {
|
|
121
|
+
color: var(--color-text-muted, #6b7280);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.error {
|
|
125
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@media (prefers-reduced-motion: reduce) {
|
|
129
|
+
.control {
|
|
130
|
+
transition: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
@property({ reflect: true })
|
|
136
|
+
size: InputSize = 'md';
|
|
137
|
+
|
|
138
|
+
@property()
|
|
139
|
+
type: InputType = 'text';
|
|
140
|
+
|
|
141
|
+
@property()
|
|
142
|
+
value = '';
|
|
143
|
+
|
|
144
|
+
@property()
|
|
145
|
+
name = '';
|
|
146
|
+
|
|
147
|
+
@property()
|
|
148
|
+
label = '';
|
|
149
|
+
|
|
150
|
+
@property()
|
|
151
|
+
placeholder = '';
|
|
152
|
+
|
|
153
|
+
@property()
|
|
154
|
+
hint = '';
|
|
155
|
+
|
|
156
|
+
@property()
|
|
157
|
+
error = '';
|
|
158
|
+
|
|
159
|
+
@property({ type: Boolean, reflect: true })
|
|
160
|
+
disabled = false;
|
|
161
|
+
|
|
162
|
+
@property({ type: Boolean, reflect: true })
|
|
163
|
+
readonly = false;
|
|
164
|
+
|
|
165
|
+
@property({ type: Boolean, reflect: true })
|
|
166
|
+
required = false;
|
|
167
|
+
|
|
168
|
+
private _id = `mfp-input-${++inputIdCounter}`;
|
|
169
|
+
|
|
170
|
+
private _onInput = (e: Event) => {
|
|
171
|
+
const input = e.target as HTMLInputElement;
|
|
172
|
+
this.value = input.value;
|
|
173
|
+
this.dispatchEvent(
|
|
174
|
+
new CustomEvent('input', {
|
|
175
|
+
bubbles: true,
|
|
176
|
+
composed: true,
|
|
177
|
+
detail: { value: input.value },
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
private _onChange = (e: Event) => {
|
|
183
|
+
const input = e.target as HTMLInputElement;
|
|
184
|
+
this.value = input.value;
|
|
185
|
+
this.dispatchEvent(
|
|
186
|
+
new CustomEvent('change', {
|
|
187
|
+
bubbles: true,
|
|
188
|
+
composed: true,
|
|
189
|
+
detail: { value: input.value },
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
override render() {
|
|
195
|
+
const invalid = this.error.length > 0;
|
|
196
|
+
const inputId = this._id;
|
|
197
|
+
const hintId = `${inputId}-hint`;
|
|
198
|
+
const errorId = `${inputId}-error`;
|
|
199
|
+
const describedBy = invalid ? errorId : this.hint ? hintId : undefined;
|
|
200
|
+
|
|
201
|
+
return html`
|
|
202
|
+
${this.label
|
|
203
|
+
? html`<label part="label" for=${inputId}>
|
|
204
|
+
${this.label}
|
|
205
|
+
${this.required
|
|
206
|
+
? html`<span class="required" aria-hidden="true">*</span>`
|
|
207
|
+
: nothing}
|
|
208
|
+
</label>`
|
|
209
|
+
: nothing}
|
|
210
|
+
<div part="control" class="control ${invalid ? 'invalid' : ''}">
|
|
211
|
+
<slot name="prefix"></slot>
|
|
212
|
+
<input
|
|
213
|
+
id=${inputId}
|
|
214
|
+
part="input"
|
|
215
|
+
type=${this.type}
|
|
216
|
+
.value=${this.value}
|
|
217
|
+
name=${this.name}
|
|
218
|
+
placeholder=${this.placeholder}
|
|
219
|
+
?disabled=${this.disabled}
|
|
220
|
+
?readonly=${this.readonly}
|
|
221
|
+
?required=${this.required}
|
|
222
|
+
aria-invalid=${invalid ? 'true' : 'false'}
|
|
223
|
+
aria-describedby=${describedBy ?? nothing}
|
|
224
|
+
@input=${this._onInput}
|
|
225
|
+
@change=${this._onChange}
|
|
226
|
+
/>
|
|
227
|
+
<slot name="suffix"></slot>
|
|
228
|
+
</div>
|
|
229
|
+
${invalid
|
|
230
|
+
? html`<p part="error" id=${errorId} class="error" role="alert">${this.error}</p>`
|
|
231
|
+
: this.hint
|
|
232
|
+
? html`<p part="hint" id=${hintId} class="hint">${this.hint}</p>`
|
|
233
|
+
: nothing}
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
declare global {
|
|
239
|
+
interface HTMLElementTagNameMap {
|
|
240
|
+
'mfp-input': MfpInput;
|
|
241
|
+
}
|
|
242
|
+
}
|