@mfp-design-system/button 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,73 @@
1
+ # @mfp-design-system/button
2
+
3
+ A Lit-based `<mfp-button>` web component. Works in any framework that supports custom elements (React, Vue, Angular, Nuxt, plain HTML).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @mfp-design-system/button @mfp-design-system/tokens
9
+ ```
10
+
11
+ `@mfp-design-system/tokens` is an optional peer dependency — the button has built-in fallback values, but loading the design tokens stylesheet gives it the canonical look.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // register the element (side-effect import)
17
+ import '@mfp-design-system/button';
18
+
19
+ // load design tokens (recommended)
20
+ import '@mfp-design-system/tokens/css';
21
+ ```
22
+
23
+ ```html
24
+ <mfp-button>Click me</mfp-button>
25
+
26
+ <mfp-button variant="primary" size="md">Save</mfp-button>
27
+ <mfp-button variant="secondary">Cancel</mfp-button>
28
+ <mfp-button variant="danger">Delete</mfp-button>
29
+ <mfp-button variant="ghost">More info</mfp-button>
30
+
31
+ <mfp-button size="sm">Small</mfp-button>
32
+ <mfp-button size="md">Medium</mfp-button>
33
+ <mfp-button size="lg">Large</mfp-button>
34
+
35
+ <mfp-button disabled>Disabled</mfp-button>
36
+ <mfp-button loading>Saving…</mfp-button>
37
+ ```
38
+
39
+ ## API
40
+
41
+ | Attribute | Type | Default | Description |
42
+ | ---------- | ------------------------------------------------- | ----------- | --------------------------------------- |
43
+ | `variant` | `'primary' \| 'secondary' \| 'danger' \| 'ghost'` | `'primary'` | Visual style |
44
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing |
45
+ | `disabled` | `boolean` | `false` | Disables the button |
46
+ | `loading` | `boolean` | `false` | Shows a spinner and disables the button |
47
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button `type` |
48
+
49
+ The button forwards clicks via the standard `click` event. The default slot accepts arbitrary content (text, icons, etc.).
50
+
51
+ For custom styling, the inner `<button>` is exposed as a CSS shadow part:
52
+
53
+ ```css
54
+ mfp-button::part(button) {
55
+ border-radius: 999px;
56
+ }
57
+ ```
58
+
59
+ ## Framework notes
60
+
61
+ - **Vue 3 / Nuxt**: tell the template compiler that `mfp-` tags are custom elements:
62
+ ```ts
63
+ // vite.config.ts or nuxt.config.ts
64
+ compilerOptions: {
65
+ isCustomElement: (tag) => tag.startsWith('mfp-'),
66
+ }
67
+ ```
68
+ - **Angular**: add `CUSTOM_ELEMENTS_SCHEMA` to any module/standalone component that uses `<mfp-button>`.
69
+ - **React**: works natively in React 19+; for older React, listen for the `click` event via a ref.
70
+
71
+ ## Known limitations
72
+
73
+ - The button does not yet participate in HTML form submission (no `ElementInternals` form-association). It works fine for click handlers; if you need it inside a `<form>` with submit semantics, wrap submission in your own handler for now. This is on the roadmap.
@@ -0,0 +1,19 @@
1
+ import { LitElement } from 'lit';
2
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
3
+ export type ButtonSize = 'sm' | 'md' | 'lg';
4
+ export type ButtonType = 'button' | 'submit' | 'reset';
5
+ export declare class MfpButton extends LitElement {
6
+ static styles: import("lit").CSSResult;
7
+ variant: ButtonVariant;
8
+ size: ButtonSize;
9
+ disabled: boolean;
10
+ loading: boolean;
11
+ type: ButtonType;
12
+ render(): import("lit").TemplateResult<1>;
13
+ }
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'mfp-button': MfpButton;
17
+ }
18
+ }
19
+ //# sourceMappingURL=button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAG5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5C,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvD,qBACa,SAAU,SAAQ,UAAU;IACvC,OAAgB,MAAM,0BAqHpB;IAGF,OAAO,EAAE,aAAa,CAAa;IAGnC,IAAI,EAAE,UAAU,CAAQ;IAGxB,QAAQ,UAAS;IAGjB,OAAO,UAAS;IAGhB,IAAI,EAAE,UAAU,CAAY;IAEnB,MAAM;CAchB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
package/dist/button.js ADDED
@@ -0,0 +1,170 @@
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 } from 'lit';
8
+ import { customElement, property } from 'lit/decorators.js';
9
+ let MfpButton = class MfpButton extends LitElement {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.variant = 'primary';
13
+ this.size = 'md';
14
+ this.disabled = false;
15
+ this.loading = false;
16
+ this.type = 'button';
17
+ }
18
+ static { this.styles = css `
19
+ :host {
20
+ display: inline-block;
21
+ }
22
+
23
+ button {
24
+ font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
25
+ font-weight: var(--font-weight-medium, 500);
26
+ line-height: var(--font-line-height-tight, 1.2);
27
+ border: 1px solid transparent;
28
+ border-radius: var(--size-radius-md, 8px);
29
+ cursor: pointer;
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ gap: var(--size-spacing-2, 8px);
34
+ white-space: nowrap;
35
+ user-select: none;
36
+ transition:
37
+ background var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
38
+ border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
39
+ color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
40
+ box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
41
+ }
42
+
43
+ button:focus-visible {
44
+ outline: 2px solid var(--color-status-info-solid, #2563eb);
45
+ outline-offset: 2px;
46
+ }
47
+
48
+ button:disabled {
49
+ cursor: not-allowed;
50
+ opacity: 0.5;
51
+ }
52
+
53
+ /* Sizes — fall back to medium when no [size] attribute is set */
54
+ :host(:not([size])) button,
55
+ :host([size='md']) button {
56
+ padding: var(--size-spacing-2, 8px) var(--size-spacing-4, 16px);
57
+ font-size: var(--font-size-base, 16px);
58
+ min-height: 40px;
59
+ }
60
+ :host([size='sm']) button {
61
+ padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
62
+ font-size: var(--font-size-sm, 14px);
63
+ min-height: 32px;
64
+ }
65
+ :host([size='lg']) button {
66
+ padding: var(--size-spacing-3, 12px) var(--size-spacing-5, 20px);
67
+ font-size: var(--font-size-lg, 18px);
68
+ min-height: 48px;
69
+ }
70
+
71
+ /* Variants — fall back to primary when no [variant] attribute is set */
72
+ :host(:not([variant])) button,
73
+ :host([variant='primary']) button {
74
+ background: var(--color-status-info-solid, #2563eb);
75
+ color: var(--color-neutral-0, #ffffff);
76
+ }
77
+ :host(:not([variant])) button:hover:not(:disabled),
78
+ :host([variant='primary']) button:hover:not(:disabled) {
79
+ background: var(--color-status-info-fg, #1e40af);
80
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
81
+ }
82
+
83
+ :host([variant='secondary']) button {
84
+ background: var(--color-neutral-0, #ffffff);
85
+ color: var(--color-text-default, #111827);
86
+ border-color: var(--color-border-default, #e5e7eb);
87
+ }
88
+ :host([variant='secondary']) button:hover:not(:disabled) {
89
+ background: var(--color-background-subtle, #f9fafb);
90
+ border-color: var(--color-border-strong, #9ca3af);
91
+ }
92
+
93
+ :host([variant='danger']) button {
94
+ background: var(--color-status-error-solid, #dc2626);
95
+ color: var(--color-neutral-0, #ffffff);
96
+ }
97
+ :host([variant='danger']) button:hover:not(:disabled) {
98
+ background: var(--color-status-error-fg, #991b1b);
99
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
100
+ }
101
+
102
+ :host([variant='ghost']) button {
103
+ background: transparent;
104
+ color: var(--color-text-default, #111827);
105
+ }
106
+ :host([variant='ghost']) button:hover:not(:disabled) {
107
+ background: var(--color-background-muted, #f3f4f6);
108
+ }
109
+
110
+ /* Loading spinner — sized relative to current font-size */
111
+ .spinner {
112
+ width: 1em;
113
+ height: 1em;
114
+ border: 2px solid currentColor;
115
+ border-top-color: transparent;
116
+ border-radius: 50%;
117
+ animation: mfp-button-spin 0.6s linear infinite;
118
+ flex: none;
119
+ }
120
+
121
+ @keyframes mfp-button-spin {
122
+ to {
123
+ transform: rotate(360deg);
124
+ }
125
+ }
126
+
127
+ @media (prefers-reduced-motion: reduce) {
128
+ button {
129
+ transition: none;
130
+ }
131
+ .spinner {
132
+ animation-duration: 1.5s;
133
+ }
134
+ }
135
+ `; }
136
+ render() {
137
+ const isInactive = this.disabled || this.loading;
138
+ return html `
139
+ <button
140
+ type=${this.type}
141
+ ?disabled=${isInactive}
142
+ aria-busy=${this.loading ? 'true' : 'false'}
143
+ part="button"
144
+ >
145
+ ${this.loading ? html `<span class="spinner" aria-hidden="true"></span>` : ''}
146
+ <slot></slot>
147
+ </button>
148
+ `;
149
+ }
150
+ };
151
+ __decorate([
152
+ property({ reflect: true })
153
+ ], MfpButton.prototype, "variant", void 0);
154
+ __decorate([
155
+ property({ reflect: true })
156
+ ], MfpButton.prototype, "size", void 0);
157
+ __decorate([
158
+ property({ type: Boolean, reflect: true })
159
+ ], MfpButton.prototype, "disabled", void 0);
160
+ __decorate([
161
+ property({ type: Boolean, reflect: true })
162
+ ], MfpButton.prototype, "loading", void 0);
163
+ __decorate([
164
+ property()
165
+ ], MfpButton.prototype, "type", void 0);
166
+ MfpButton = __decorate([
167
+ customElement('mfp-button')
168
+ ], MfpButton);
169
+ export { MfpButton };
170
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.js","sourceRoot":"","sources":["../src/button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAOrD,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;IAAlC;;QAyHL,YAAO,GAAkB,SAAS,CAAC;QAGnC,SAAI,GAAe,IAAI,CAAC;QAGxB,aAAQ,GAAG,KAAK,CAAC;QAGjB,YAAO,GAAG,KAAK,CAAC;QAGhB,SAAI,GAAe,QAAQ,CAAC;IAgB9B,CAAC;aApJiB,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqH3B,AArHqB,CAqHpB;IAiBO,MAAM;QACb,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC;QACjD,OAAO,IAAI,CAAA;;eAEA,IAAI,CAAC,IAAI;oBACJ,UAAU;oBACV,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;;;UAGzC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,kDAAkD,CAAC,CAAC,CAAC,EAAE;;;KAG/E,CAAC;IACJ,CAAC;;AA3BD;IADC,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0CACO;AAGnC;IADC,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;uCACJ;AAGxB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;2CAC1B;AAGjB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;0CAC3B;AAGhB;IADC,QAAQ,EAAE;uCACiB;AArIjB,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CAqJrB"}
@@ -0,0 +1,3 @@
1
+ export { MfpButton } from './button.js';
2
+ export type { ButtonVariant, ButtonSize, ButtonType } from './button.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,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { MfpButton } from './button.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,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@mfp-design-system/button",
3
+ "version": "0.1.0",
4
+ "description": "Button 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
+ }
@@ -0,0 +1,113 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components';
2
+ import { html } from 'lit';
3
+ import './button.js';
4
+ import type { ButtonSize, ButtonVariant } from './button.js';
5
+
6
+ interface Args {
7
+ variant: ButtonVariant;
8
+ size: ButtonSize;
9
+ disabled: boolean;
10
+ loading: boolean;
11
+ label: string;
12
+ }
13
+
14
+ const meta: Meta<Args> = {
15
+ title: 'Components/Button',
16
+ component: 'mfp-button',
17
+ tags: ['autodocs'],
18
+ argTypes: {
19
+ variant: {
20
+ control: 'select',
21
+ options: ['primary', 'secondary', 'danger', 'ghost'],
22
+ description: 'Visual style',
23
+ },
24
+ size: {
25
+ control: 'select',
26
+ options: ['sm', 'md', 'lg'],
27
+ description: 'Sizing',
28
+ },
29
+ disabled: { control: 'boolean' },
30
+ loading: { control: 'boolean' },
31
+ label: { control: 'text', description: 'Button label (slot content)' },
32
+ },
33
+ args: {
34
+ variant: 'primary',
35
+ size: 'md',
36
+ disabled: false,
37
+ loading: false,
38
+ label: 'Button',
39
+ },
40
+ render: (args) => html`
41
+ <mfp-button
42
+ variant=${args.variant}
43
+ size=${args.size}
44
+ ?disabled=${args.disabled}
45
+ ?loading=${args.loading}
46
+ >
47
+ ${args.label}
48
+ </mfp-button>
49
+ `,
50
+ };
51
+
52
+ export default meta;
53
+
54
+ type Story = StoryObj<Args>;
55
+
56
+ export const Primary: Story = {
57
+ args: { variant: 'primary', label: 'Primary' },
58
+ };
59
+
60
+ export const Secondary: Story = {
61
+ args: { variant: 'secondary', label: 'Secondary' },
62
+ };
63
+
64
+ export const Danger: Story = {
65
+ args: { variant: 'danger', label: 'Delete' },
66
+ };
67
+
68
+ export const Ghost: Story = {
69
+ args: { variant: 'ghost', label: 'More info' },
70
+ };
71
+
72
+ export const Sizes: Story = {
73
+ parameters: { controls: { disable: true } },
74
+ render: () => html`
75
+ <div style="display: flex; gap: 12px; align-items: center;">
76
+ <mfp-button size="sm">Small</mfp-button>
77
+ <mfp-button size="md">Medium</mfp-button>
78
+ <mfp-button size="lg">Large</mfp-button>
79
+ </div>
80
+ `,
81
+ };
82
+
83
+ export const AllVariants: Story = {
84
+ parameters: { controls: { disable: true } },
85
+ render: () => html`
86
+ <div style="display: flex; gap: 12px; flex-wrap: wrap;">
87
+ <mfp-button variant="primary">Primary</mfp-button>
88
+ <mfp-button variant="secondary">Secondary</mfp-button>
89
+ <mfp-button variant="danger">Danger</mfp-button>
90
+ <mfp-button variant="ghost">Ghost</mfp-button>
91
+ </div>
92
+ `,
93
+ };
94
+
95
+ export const Disabled: Story = {
96
+ args: { disabled: true, label: 'Disabled' },
97
+ };
98
+
99
+ export const Loading: Story = {
100
+ args: { loading: true, label: 'Saving…' },
101
+ };
102
+
103
+ export const LoadingStates: Story = {
104
+ parameters: { controls: { disable: true } },
105
+ render: () => html`
106
+ <div style="display: flex; gap: 12px; flex-wrap: wrap;">
107
+ <mfp-button variant="primary" loading>Saving…</mfp-button>
108
+ <mfp-button variant="secondary" loading>Loading</mfp-button>
109
+ <mfp-button variant="danger" loading>Deleting</mfp-button>
110
+ <mfp-button variant="ghost" loading>…</mfp-button>
111
+ </div>
112
+ `,
113
+ };
package/src/button.ts ADDED
@@ -0,0 +1,164 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
5
+ export type ButtonSize = 'sm' | 'md' | 'lg';
6
+ export type ButtonType = 'button' | 'submit' | 'reset';
7
+
8
+ @customElement('mfp-button')
9
+ export class MfpButton extends LitElement {
10
+ static override styles = css`
11
+ :host {
12
+ display: inline-block;
13
+ }
14
+
15
+ button {
16
+ font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
17
+ font-weight: var(--font-weight-medium, 500);
18
+ line-height: var(--font-line-height-tight, 1.2);
19
+ border: 1px solid transparent;
20
+ border-radius: var(--size-radius-md, 8px);
21
+ cursor: pointer;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ gap: var(--size-spacing-2, 8px);
26
+ white-space: nowrap;
27
+ user-select: none;
28
+ transition:
29
+ background var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
30
+ border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
31
+ color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
32
+ box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
33
+ }
34
+
35
+ button:focus-visible {
36
+ outline: 2px solid var(--color-status-info-solid, #2563eb);
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ button:disabled {
41
+ cursor: not-allowed;
42
+ opacity: 0.5;
43
+ }
44
+
45
+ /* Sizes — fall back to medium when no [size] attribute is set */
46
+ :host(:not([size])) button,
47
+ :host([size='md']) button {
48
+ padding: var(--size-spacing-2, 8px) var(--size-spacing-4, 16px);
49
+ font-size: var(--font-size-base, 16px);
50
+ min-height: 40px;
51
+ }
52
+ :host([size='sm']) button {
53
+ padding: var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
54
+ font-size: var(--font-size-sm, 14px);
55
+ min-height: 32px;
56
+ }
57
+ :host([size='lg']) button {
58
+ padding: var(--size-spacing-3, 12px) var(--size-spacing-5, 20px);
59
+ font-size: var(--font-size-lg, 18px);
60
+ min-height: 48px;
61
+ }
62
+
63
+ /* Variants — fall back to primary when no [variant] attribute is set */
64
+ :host(:not([variant])) button,
65
+ :host([variant='primary']) button {
66
+ background: var(--color-status-info-solid, #2563eb);
67
+ color: var(--color-neutral-0, #ffffff);
68
+ }
69
+ :host(:not([variant])) button:hover:not(:disabled),
70
+ :host([variant='primary']) button:hover:not(:disabled) {
71
+ background: var(--color-status-info-fg, #1e40af);
72
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
73
+ }
74
+
75
+ :host([variant='secondary']) button {
76
+ background: var(--color-neutral-0, #ffffff);
77
+ color: var(--color-text-default, #111827);
78
+ border-color: var(--color-border-default, #e5e7eb);
79
+ }
80
+ :host([variant='secondary']) button:hover:not(:disabled) {
81
+ background: var(--color-background-subtle, #f9fafb);
82
+ border-color: var(--color-border-strong, #9ca3af);
83
+ }
84
+
85
+ :host([variant='danger']) button {
86
+ background: var(--color-status-error-solid, #dc2626);
87
+ color: var(--color-neutral-0, #ffffff);
88
+ }
89
+ :host([variant='danger']) button:hover:not(:disabled) {
90
+ background: var(--color-status-error-fg, #991b1b);
91
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.08));
92
+ }
93
+
94
+ :host([variant='ghost']) button {
95
+ background: transparent;
96
+ color: var(--color-text-default, #111827);
97
+ }
98
+ :host([variant='ghost']) button:hover:not(:disabled) {
99
+ background: var(--color-background-muted, #f3f4f6);
100
+ }
101
+
102
+ /* Loading spinner — sized relative to current font-size */
103
+ .spinner {
104
+ width: 1em;
105
+ height: 1em;
106
+ border: 2px solid currentColor;
107
+ border-top-color: transparent;
108
+ border-radius: 50%;
109
+ animation: mfp-button-spin 0.6s linear infinite;
110
+ flex: none;
111
+ }
112
+
113
+ @keyframes mfp-button-spin {
114
+ to {
115
+ transform: rotate(360deg);
116
+ }
117
+ }
118
+
119
+ @media (prefers-reduced-motion: reduce) {
120
+ button {
121
+ transition: none;
122
+ }
123
+ .spinner {
124
+ animation-duration: 1.5s;
125
+ }
126
+ }
127
+ `;
128
+
129
+ @property({ reflect: true })
130
+ variant: ButtonVariant = 'primary';
131
+
132
+ @property({ reflect: true })
133
+ size: ButtonSize = 'md';
134
+
135
+ @property({ type: Boolean, reflect: true })
136
+ disabled = false;
137
+
138
+ @property({ type: Boolean, reflect: true })
139
+ loading = false;
140
+
141
+ @property()
142
+ type: ButtonType = 'button';
143
+
144
+ override render() {
145
+ const isInactive = this.disabled || this.loading;
146
+ return html`
147
+ <button
148
+ type=${this.type}
149
+ ?disabled=${isInactive}
150
+ aria-busy=${this.loading ? 'true' : 'false'}
151
+ part="button"
152
+ >
153
+ ${this.loading ? html`<span class="spinner" aria-hidden="true"></span>` : ''}
154
+ <slot></slot>
155
+ </button>
156
+ `;
157
+ }
158
+ }
159
+
160
+ declare global {
161
+ interface HTMLElementTagNameMap {
162
+ 'mfp-button': MfpButton;
163
+ }
164
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { MfpButton } from './button.js';
2
+ export type { ButtonVariant, ButtonSize, ButtonType } from './button.js';