@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 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.
@@ -0,0 +1,3 @@
1
+ export { MfpInput } from './input.js';
2
+ export type { InputSize, InputType } from './input.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { MfpInput } from './input.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
@@ -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,2 @@
1
+ export { MfpInput } from './input.js';
2
+ export type { InputSize, InputType } from './input.js';
@@ -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&rsquo;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
+ }