@kubex/zinc 1.0.25 → 1.0.99
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/.claude/project.md +556 -0
- package/.github/workflows/js_build_and_deploy.yaml +9 -10
- package/dist/custom-elements.json +13575 -8856
- package/dist/vscode.html-custom-data.json +583 -48
- package/dist/web-types.json +1441 -181
- package/dist/zn.d.ts +1645 -660
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +1412 -893
- package/docs/_utilities/code-previews.cjs +10 -1
- package/docs/assets/scripts/docs.js +42 -18
- package/docs/data/data-table.json +229 -66
- package/docs/data/empty.json +6 -0
- package/docs/data/products-table.json +41 -193
- package/docs/pages/components/action-bar.md +121 -6
- package/docs/pages/components/alert.md +118 -15
- package/docs/pages/components/animated-button.md +299 -0
- package/docs/pages/components/audio-select.md +534 -0
- package/docs/pages/components/bulk-actions.md +79 -5
- package/docs/pages/components/button-group.md +235 -27
- package/docs/pages/components/button.md +139 -1
- package/docs/pages/components/chart.md +365 -9
- package/docs/pages/components/checkbox.md +387 -58
- package/docs/pages/components/chip.md +253 -33
- package/docs/pages/components/collapsible.md +393 -46
- package/docs/pages/components/cols.md +152 -30
- package/docs/pages/components/confirm.md +182 -16
- package/docs/pages/components/copy-button.md +169 -6
- package/docs/pages/components/data-select.md +194 -15
- package/docs/pages/components/data-table-filter.md +96 -5
- package/docs/pages/components/data-table-search.md +451 -0
- package/docs/pages/components/data-table-sort.md +66 -5
- package/docs/pages/components/data-table.md +727 -66
- package/docs/pages/components/datepicker.md +402 -7
- package/docs/pages/components/defined-label.md +120 -5
- package/docs/pages/components/dialog.md +339 -59
- package/docs/pages/components/dropdown.md +366 -11
- package/docs/pages/components/editor.md +604 -7
- package/docs/pages/components/empty-state.md +315 -16
- package/docs/pages/components/expanding-action.md +212 -28
- package/docs/pages/components/file.md +421 -12
- package/docs/pages/components/filter-container.md +158 -5
- package/docs/pages/components/filter-wrapper.md +167 -6
- package/docs/pages/components/form-group.md +383 -6
- package/docs/pages/components/hover-container.md +237 -6
- package/docs/pages/components/icon-picker.md +121 -0
- package/docs/pages/components/icon.md +454 -48
- package/docs/pages/components/inline-edit.md +787 -19
- package/docs/pages/components/input-group.md +207 -13
- package/docs/pages/components/input.md +194 -0
- package/docs/pages/components/item.md +18 -0
- package/docs/pages/components/key-container.md +115 -0
- package/docs/pages/components/key.md +101 -0
- package/docs/pages/components/menu.md +512 -6
- package/docs/pages/components/note.md +239 -36
- package/docs/pages/components/opt-group.md +107 -0
- package/docs/pages/components/pagination.md +332 -6
- package/docs/pages/components/pane.md +222 -6
- package/docs/pages/components/panel.md +213 -26
- package/docs/pages/components/popup.md +1354 -6
- package/docs/pages/components/priority-list.md +233 -0
- package/docs/pages/components/progress-bar.md +387 -6
- package/docs/pages/components/radio-group.md +242 -5
- package/docs/pages/components/radio.md +474 -6
- package/docs/pages/components/rating.md +331 -7
- package/docs/pages/components/reveal.md +524 -6
- package/docs/pages/components/scroll-container.md +552 -6
- package/docs/pages/components/select.md +762 -160
- package/docs/pages/components/skeleton.md +473 -30
- package/docs/pages/components/slideout.md +261 -5
- package/docs/pages/components/sp.md +99 -41
- package/docs/pages/components/split-button.md +347 -9
- package/docs/pages/components/split-pane.md +445 -6
- package/docs/pages/components/stat.md +549 -9
- package/docs/pages/components/status-indicator.md +230 -1
- package/docs/pages/components/stepper.md +563 -6
- package/docs/pages/components/tabs.md +439 -13
- package/docs/pages/components/textarea.md +415 -17
- package/docs/pages/components/tile.md +385 -26
- package/docs/pages/components/toggle.md +424 -6
- package/docs/pages/components/tooltip.md +367 -0
- package/docs/pages/components/translation-group.md +210 -0
- package/docs/pages/components/translations.md +319 -19
- package/docs/pages/components/vertical-stepper.md +401 -8
- package/docs/pages/components/well.md +300 -3
- package/docs/pages/getting-started/form-controls.md +162 -0
- package/docs/pages/index.md +116 -15
- package/package.json +5 -6
- package/scss/_global-spacing.scss +4 -0
- package/scss/_root.scss +2 -2
- package/scss/boot.scss +19 -0
- package/scss/shared/layout.scss +5 -0
- package/scss/themes/_light.scss +3 -3
- package/src/components/alert/alert.scss +1 -0
- package/src/components/animated-button/README.md +306 -0
- package/src/components/animated-button/animated-button.component.ts +229 -0
- package/src/components/animated-button/animated-button.scss +228 -0
- package/src/components/animated-button/animated-button.test.ts +143 -0
- package/src/components/animated-button/index.ts +12 -0
- package/src/components/button/button.component.ts +5 -0
- package/src/components/button-group/button-group.component.ts +25 -13
- package/src/components/button-group/button-group.scss +6 -0
- package/src/components/checkbox/checkbox.component.ts +4 -0
- package/src/components/checkbox/checkbox.scss +7 -4
- package/src/components/checkbox-group/checkbox-group.scss +7 -0
- package/src/components/chip/chip.component.ts +7 -2
- package/src/components/chip/chip.scss +16 -2
- package/src/components/collapsible/collapsible.component.ts +27 -20
- package/src/components/cols/cols.component.ts +14 -2
- package/src/components/cols/cols.scss +6 -12
- package/src/components/confirm/confirm.component.ts +7 -0
- package/src/components/copy-button/copy-button.component.ts +12 -11
- package/src/components/data-select/data-select.component.ts +215 -44
- package/src/components/data-select/data-select.scss +66 -6
- package/src/components/data-select/providers/currency-data-provider.ts +20 -6
- package/src/components/data-select/providers/provider.ts +1 -0
- package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
- package/src/components/data-table/data-table.component.ts +81 -22
- package/src/components/data-table-search/data-table-search.component.ts +184 -0
- package/src/components/data-table-search/data-table-search.scss +17 -0
- package/src/components/data-table-search/data-table-search.test.ts +11 -0
- package/src/components/data-table-search/index.ts +12 -0
- package/src/components/datepicker/datepicker.component.ts +347 -13
- package/src/components/dialog/dialog.component.ts +1 -1
- package/src/components/empty-state/empty-state.scss +6 -1
- package/src/components/header/header.scss +3 -3
- package/src/components/icon/icon.component.ts +23 -1
- package/src/components/icon/icon.scss +23 -0
- package/src/components/icon-picker/brand-icons.ts +24 -0
- package/src/components/icon-picker/icon-picker.component.ts +407 -0
- package/src/components/icon-picker/icon-picker.scss +205 -0
- package/src/components/icon-picker/index.ts +12 -0
- package/src/components/icon-picker/line-icons.ts +612 -0
- package/src/components/icon-picker/material-icons.ts +14543 -0
- package/src/components/inline-edit/inline-edit.component.ts +190 -46
- package/src/components/inline-edit/inline-edit.scss +29 -0
- package/src/components/inline-edit/inline-edit.test.ts +314 -1
- package/src/components/input/input.component.ts +324 -5
- package/src/components/input/input.scss +87 -0
- package/src/components/input-group/input-group.component.ts +28 -7
- package/src/components/input-group/input-group.scss +48 -10
- package/src/components/item/item.component.ts +24 -3
- package/src/components/item/item.scss +18 -0
- package/src/components/key/index.ts +12 -0
- package/src/components/key/key.component.ts +47 -0
- package/src/components/key/key.scss +38 -0
- package/src/components/key-container/index.ts +12 -0
- package/src/components/key-container/key-container.component.ts +142 -0
- package/src/components/key-container/key-container.scss +33 -0
- package/src/components/menu-item/menu-item.component.ts +20 -0
- package/src/components/menu-item/menu-item.scss +69 -2
- package/src/components/navbar/navbar.component.ts +103 -11
- package/src/components/navbar/navbar.scss +6 -1
- package/src/components/opt-group/index.ts +12 -0
- package/src/components/opt-group/opt-group.component.ts +74 -0
- package/src/components/opt-group/opt-group.scss +37 -0
- package/src/components/opt-group/opt-group.test.ts +27 -0
- package/src/components/option/option.component.ts +1 -16
- package/src/components/option/option.scss +4 -0
- package/src/components/panel/panel.component.ts +10 -8
- package/src/components/panel/panel.scss +20 -0
- package/src/components/priority-list/index.ts +12 -0
- package/src/components/priority-list/priority-list.component.ts +611 -0
- package/src/components/priority-list/priority-list.scss +185 -0
- package/src/components/priority-list/priority-list.test.ts +139 -0
- package/src/components/progress-bar/progress-bar.component.ts +22 -16
- package/src/components/radio/radio.scss +10 -3
- package/src/components/select/select.component.ts +812 -72
- package/src/components/select/select.scss +108 -2
- package/src/components/select/select.test.ts +147 -2
- package/src/components/settings-container/settings-container.component.ts +44 -19
- package/src/components/settings-container/settings-container.scss +1 -1
- package/src/components/skeleton/skeleton.component.ts +6 -1
- package/src/components/slideout/slideout.scss +8 -3
- package/src/components/sp/sp.component.ts +3 -1
- package/src/components/sp/sp.scss +19 -0
- package/src/components/stepper/stepper.component.ts +16 -10
- package/src/components/textarea/textarea.scss +0 -4
- package/src/components/tile/tile.component.ts +12 -3
- package/src/components/tile/tile.scss +1 -1
- package/src/components/timer/timer.component.ts +15 -2
- package/src/components/toggle/toggle.component.ts +29 -26
- package/src/components/toggle/toggle.scss +13 -1
- package/src/components/translation-group/index.ts +12 -0
- package/src/components/translation-group/translation-group.component.ts +207 -0
- package/src/components/translation-group/translation-group.scss +8 -0
- package/src/components/translations/translations.component.ts +90 -44
- package/src/components/translations/translations.scss +4 -0
- package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
- package/src/events/events.ts +12 -7
- package/src/events/zn-language-change.ts +7 -0
- package/src/events/zn-purchase.ts +11 -0
- package/src/events/zn-redirect.ts +7 -0
- package/src/events/zn-reorder.ts +7 -0
- package/src/events/zn-search-change.ts +11 -0
- package/src/events/zn-submit.ts +1 -1
- package/src/form-control.scss +4 -0
- package/src/internal/form-navigation.ts +510 -0
- package/src/internal/form.ts +6 -0
- package/src/zinc.ts +91 -83
package/scss/_root.scss
CHANGED
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
--zn-shadow: 10, 10, 10;
|
|
84
84
|
--zn-panel-secondary: 27, 27, 30;
|
|
85
85
|
--zn-panel-opacity: 1;
|
|
86
|
-
--zn-text:
|
|
86
|
+
--zn-text: 159, 173, 194;
|
|
87
87
|
--zn-text-heading: 241, 245, 249;
|
|
88
88
|
--zn-text-page-title: 225, 225, 225;
|
|
89
89
|
--zn-text-panel-title: 181, 158, 255;
|
|
90
|
-
--zn-panel-highlight:
|
|
90
|
+
--zn-panel-highlight: 25, 27, 32;
|
|
91
91
|
--zn-panel-highlight-opacity: 0.1;
|
|
92
92
|
--zn-border-color: 50, 50, 60;
|
|
93
93
|
--zn-input-bg: 28, 28, 32;
|
package/scss/boot.scss
CHANGED
|
@@ -40,6 +40,25 @@
|
|
|
40
40
|
background-color: var(--zn-input-background-color);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
.air-datepicker-global-container {
|
|
44
|
+
z-index: 1000000;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.air-datepicker-button {
|
|
48
|
+
border: none;
|
|
49
|
+
padding: var(--zn-spacing-2x-small) var(--zn-spacing-x-small);
|
|
50
|
+
border-radius: var(--adp-border-radius);
|
|
51
|
+
font-family: var(--adp-font-family), sans-serif;
|
|
52
|
+
font-size: var(--zn-font-size-medium);
|
|
53
|
+
font-weight: var(--zn-font-weight-normal);
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
color: rgba(var(--zn-text), var(--zn-text-opacity));
|
|
56
|
+
|
|
57
|
+
&:hover {
|
|
58
|
+
background-color: var(--zn-input-background-color-hover) !important;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
// Utility to minimize flashing of undefined web components
|
|
44
63
|
.zn-cloak:has(:not(:defined)) {
|
|
45
64
|
animation: 2s step-end zn-cloak;
|
package/scss/shared/layout.scss
CHANGED
package/scss/themes/_light.scss
CHANGED
|
@@ -390,8 +390,8 @@
|
|
|
390
390
|
--zn-font-size-x-small: 0.75rem; // 12px // 10.5px
|
|
391
391
|
--zn-font-size-small: 0.929rem; // 14px // 13px
|
|
392
392
|
--zn-font-size-medium: 1rem; // 16px // 14px
|
|
393
|
-
--zn-font-size-large: 1.
|
|
394
|
-
--zn-font-size-x-large: 1.
|
|
393
|
+
--zn-font-size-large: 1.15rem; // 20px // 16px
|
|
394
|
+
--zn-font-size-x-large: 1.25rem; // 24px // 21px
|
|
395
395
|
--zn-font-size-2x-large: 2.25rem; // 36px // 31.5px
|
|
396
396
|
--zn-font-size-3x-large: 3rem; // 48px // 42px
|
|
397
397
|
--zn-font-size-4x-large: 4.5rem; // 72px // 63px
|
|
@@ -512,7 +512,7 @@
|
|
|
512
512
|
--zn-toggle-size-large: 1.375rem; // 22px
|
|
513
513
|
|
|
514
514
|
/* Overlays */
|
|
515
|
-
--zn-overlay-background-color: hsl(
|
|
515
|
+
--zn-overlay-background-color: hsl(none 0% 0% / 0.7);
|
|
516
516
|
|
|
517
517
|
/* Panels */
|
|
518
518
|
--zn-panel-background-color: var(--zn-color-neutral-0);
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Animated Button Component
|
|
2
|
+
|
|
3
|
+
A stateful button component designed for purchase/payment flows with built-in loading, success, and error states, complete with smooth animations and automatic state management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **4 States**: Idle (blue), Processing (blue with shimmer), Success (green with animated fill and drawing checkmark), Failure (red with error)
|
|
8
|
+
- **Smooth Animations**:
|
|
9
|
+
- Success state: Background fills from bottom with green color
|
|
10
|
+
- Checkmark draws itself with a satisfying "swoosh" effect
|
|
11
|
+
- Shimmer animation during processing
|
|
12
|
+
- Shake animation on failure
|
|
13
|
+
- **Auto State Management**: Automatically resets from failure state after a configurable delay
|
|
14
|
+
- **Auto Redirect**: Redirects after successful purchase with configurable delay
|
|
15
|
+
- **Event-Driven**: Emit custom events for purchase flow integration
|
|
16
|
+
- **Accessible**: Supports `aria-live` regions and respects `prefers-reduced-motion`
|
|
17
|
+
- **Consistent Height**: Button maintains the same height across all states
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import '@kubex/zinc/components/animated-button';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or if auto-loading is enabled, just use the tag:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<zn-animated-button></zn-animated-button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Usage
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<zn-animated-button id="purchase-btn"></zn-animated-button>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
const button = document.getElementById('purchase-btn');
|
|
38
|
+
|
|
39
|
+
button.addEventListener('zn-purchase', async (event) => {
|
|
40
|
+
const { setSuccess, setFailure } = event.detail;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch('/api/purchase', { method: 'POST' });
|
|
44
|
+
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
setSuccess();
|
|
47
|
+
} else {
|
|
48
|
+
setFailure('Payment failed');
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
setFailure(error.message);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Properties
|
|
58
|
+
|
|
59
|
+
| Property | Type | Default | Description |
|
|
60
|
+
|----------|------|---------|-------------|
|
|
61
|
+
| `idle-text` | `string` | `'Purchase'` | Text displayed in idle state |
|
|
62
|
+
| `processing-text` | `string` | `'Purchasing'` | Text displayed in processing state |
|
|
63
|
+
| `success-text` | `string` | `'Success'` | Text displayed in success state |
|
|
64
|
+
| `failure-text` | `string` | `'Failed'` | Text displayed in failure state |
|
|
65
|
+
| `redirect-url` | `string` | `''` | URL to redirect to after success |
|
|
66
|
+
| `redirect-delay` | `number` | `1500` | Milliseconds to wait before redirecting (after success) |
|
|
67
|
+
| `failure-reset-delay` | `number` | `2000` | Milliseconds to wait before auto-resetting from failure |
|
|
68
|
+
| `disabled` | `boolean` | `false` | Whether the button is disabled |
|
|
69
|
+
| `name` | `string` | `''` | Form control name |
|
|
70
|
+
| `value` | `string` | `''` | Form control value |
|
|
71
|
+
|
|
72
|
+
## Methods
|
|
73
|
+
|
|
74
|
+
### `purchase(): Promise<void>`
|
|
75
|
+
|
|
76
|
+
Programmatically trigger the purchase flow.
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
const button = document.getElementById('purchase-btn');
|
|
80
|
+
await button.purchase();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `setSuccess(): void`
|
|
84
|
+
|
|
85
|
+
Manually set the button to success state. Triggers the redirect timer.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
button.setSuccess();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `setFailure(message?: string): void`
|
|
92
|
+
|
|
93
|
+
Manually set the button to failure state with an optional error message. Triggers the auto-reset timer.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
button.setFailure('Payment declined');
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `reset(): void`
|
|
100
|
+
|
|
101
|
+
Reset the button to idle state, clearing any timers.
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
button.reset();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Events
|
|
108
|
+
|
|
109
|
+
### `zn-purchase`
|
|
110
|
+
|
|
111
|
+
Emitted when the button is clicked in idle state. The event detail contains:
|
|
112
|
+
|
|
113
|
+
- `value`: The form control value
|
|
114
|
+
- `setSuccess()`: Function to set success state
|
|
115
|
+
- `setFailure(message?: string)`: Function to set failure state
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
button.addEventListener('zn-purchase', (event) => {
|
|
119
|
+
const { value, setSuccess, setFailure } = event.detail;
|
|
120
|
+
|
|
121
|
+
// Implement purchase logic
|
|
122
|
+
if (purchaseSuccessful) {
|
|
123
|
+
setSuccess();
|
|
124
|
+
} else {
|
|
125
|
+
setFailure('Error message');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Note**: The event is cancelable. Call `event.preventDefault()` to prevent the state transition.
|
|
131
|
+
|
|
132
|
+
### `zn-redirect`
|
|
133
|
+
|
|
134
|
+
Emitted when the redirect timer completes after success. The event detail contains:
|
|
135
|
+
|
|
136
|
+
- `url`: The redirect URL
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
button.addEventListener('zn-redirect', (event) => {
|
|
140
|
+
console.log('Redirecting to:', event.detail.url);
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## State Flow
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
┌─────────┐
|
|
148
|
+
│ Idle │ (Blue)
|
|
149
|
+
│ Default │
|
|
150
|
+
└────┬────┘
|
|
151
|
+
│ Click
|
|
152
|
+
↓
|
|
153
|
+
┌─────────┐
|
|
154
|
+
│Processing│ (Blue + Shimmer)
|
|
155
|
+
│ Disabled │
|
|
156
|
+
└────┬────┘
|
|
157
|
+
│
|
|
158
|
+
┌─────┴─────┐
|
|
159
|
+
│ │
|
|
160
|
+
↓ ↓
|
|
161
|
+
┌─────────┐ ┌─────────┐
|
|
162
|
+
│Success │ │ Failure │ (Red + Error Icon)
|
|
163
|
+
│(Green + │ │ Disabled│
|
|
164
|
+
│ Check) │ └────┬────┘
|
|
165
|
+
└────┬────┘ │
|
|
166
|
+
│ │ Auto-reset (2s)
|
|
167
|
+
│ ↓
|
|
168
|
+
│ ┌─────────┐
|
|
169
|
+
│ │ Idle │
|
|
170
|
+
│ └─────────┘
|
|
171
|
+
│
|
|
172
|
+
│ Redirect (1.5s)
|
|
173
|
+
↓
|
|
174
|
+
[Redirect]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Styling
|
|
178
|
+
|
|
179
|
+
The component uses CSS custom properties from the Zinc design system:
|
|
180
|
+
|
|
181
|
+
- `--zn-primary`: Primary color (blue, idle state)
|
|
182
|
+
- `--zn-color-success`: Success color (green)
|
|
183
|
+
- `--zn-color-error`: Error color (red)
|
|
184
|
+
- `--zn-transition-medium`: Transition duration for color changes
|
|
185
|
+
- `--zn-transition-fast`: Transition duration for interactions
|
|
186
|
+
- `--zn-border-radius-medium`: Border radius
|
|
187
|
+
|
|
188
|
+
### CSS Parts
|
|
189
|
+
|
|
190
|
+
The component doesn't expose CSS parts but follows BEM naming:
|
|
191
|
+
|
|
192
|
+
- `.purchase-button`: Main button element
|
|
193
|
+
- `.purchase-button--idle`: Idle state modifier
|
|
194
|
+
- `.purchase-button--processing`: Processing state modifier
|
|
195
|
+
- `.purchase-button--success`: Success state modifier
|
|
196
|
+
- `.purchase-button--failure`: Failure state modifier
|
|
197
|
+
- `.purchase-button__content`: Content wrapper
|
|
198
|
+
- `.purchase-button__text`: Text element
|
|
199
|
+
- `.purchase-button__spinner`: Spinning loading icon
|
|
200
|
+
- `.purchase-button__shimmer`: Shimmer animation overlay
|
|
201
|
+
- `.purchase-button__icon`: Icon element
|
|
202
|
+
|
|
203
|
+
## Accessibility
|
|
204
|
+
|
|
205
|
+
- The button includes `aria-live="polite"` for screen reader announcements of state changes
|
|
206
|
+
- The button includes `aria-busy` attribute during processing state
|
|
207
|
+
- Respects `prefers-reduced-motion` media query:
|
|
208
|
+
- Disables all animations including success fill, checkmark drawing, and shimmer
|
|
209
|
+
- Shows instant state changes for users who prefer reduced motion
|
|
210
|
+
- Proper focus states with visible outline
|
|
211
|
+
- Disabled state prevents interaction and is properly announced
|
|
212
|
+
- Consistent button height prevents layout shift that could confuse users
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
### Simple Success Flow
|
|
217
|
+
|
|
218
|
+
```html
|
|
219
|
+
<zn-animated-button id="btn"></zn-animated-button>
|
|
220
|
+
|
|
221
|
+
<script>
|
|
222
|
+
document.getElementById('btn').addEventListener('zn-purchase', async (e) => {
|
|
223
|
+
const { setSuccess } = e.detail;
|
|
224
|
+
await new Promise(r => setTimeout(r, 2000)); // Simulate API
|
|
225
|
+
setSuccess();
|
|
226
|
+
});
|
|
227
|
+
</script>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### With Error Handling
|
|
231
|
+
|
|
232
|
+
```html
|
|
233
|
+
<zn-animated-button
|
|
234
|
+
id="btn"
|
|
235
|
+
failure-text="Payment Failed"
|
|
236
|
+
></zn-animated-button>
|
|
237
|
+
|
|
238
|
+
<script>
|
|
239
|
+
document.getElementById('btn').addEventListener('zn-purchase', async (e) => {
|
|
240
|
+
const { setSuccess, setFailure } = e.detail;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch('/api/purchase', { method: 'POST' });
|
|
244
|
+
const data = await response.json();
|
|
245
|
+
|
|
246
|
+
if (response.ok) {
|
|
247
|
+
setSuccess();
|
|
248
|
+
} else {
|
|
249
|
+
setFailure(data.error || 'Payment failed');
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
setFailure('Network error');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
</script>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Custom Text and Timing
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<zn-animated-button
|
|
262
|
+
idle-text="Complete Purchase"
|
|
263
|
+
processing-text="Processing Payment"
|
|
264
|
+
success-text="Payment Complete"
|
|
265
|
+
failure-text="Transaction Failed"
|
|
266
|
+
redirect-url="/thank-you"
|
|
267
|
+
redirect-delay="3000"
|
|
268
|
+
failure-reset-delay="4000"
|
|
269
|
+
></zn-animated-button>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Manual Control
|
|
273
|
+
|
|
274
|
+
```javascript
|
|
275
|
+
const button = document.getElementById('purchase-btn');
|
|
276
|
+
|
|
277
|
+
// Trigger purchase programmatically
|
|
278
|
+
await button.purchase();
|
|
279
|
+
|
|
280
|
+
// Or control state directly
|
|
281
|
+
button.setSuccess();
|
|
282
|
+
button.setFailure('Insufficient funds');
|
|
283
|
+
button.reset();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Dependencies
|
|
287
|
+
|
|
288
|
+
- `zn-icon`: Icon component for spinner, success, and failure icons
|
|
289
|
+
|
|
290
|
+
## Browser Support
|
|
291
|
+
|
|
292
|
+
Works in all modern browsers that support:
|
|
293
|
+
- Web Components (Custom Elements)
|
|
294
|
+
- CSS Custom Properties
|
|
295
|
+
- CSS Animations
|
|
296
|
+
- ES Modules
|
|
297
|
+
|
|
298
|
+
## Testing
|
|
299
|
+
|
|
300
|
+
Run tests with:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npm test
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
See `purchase-button.test.ts` for test examples.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {classMap} from 'lit/directives/class-map.js';
|
|
2
|
+
import {type CSSResultGroup, html, unsafeCSS} from 'lit';
|
|
3
|
+
import {property, state} from 'lit/decorators.js';
|
|
4
|
+
import ZincElement from '../../internal/zinc-element';
|
|
5
|
+
import ZnIcon from '../icon/icon.component';
|
|
6
|
+
|
|
7
|
+
import styles from './animated-button.scss';
|
|
8
|
+
|
|
9
|
+
type AnimatedButtonState = 'idle' | 'processing' | 'success' | 'failure';
|
|
10
|
+
|
|
11
|
+
export default class ZnAnimatedButton extends ZincElement {
|
|
12
|
+
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
13
|
+
|
|
14
|
+
static dependencies = {
|
|
15
|
+
'zn-icon': ZnIcon
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
@state() private currentState: AnimatedButtonState = 'idle';
|
|
19
|
+
@state() private errorMessage = '';
|
|
20
|
+
|
|
21
|
+
/** The text to display in the idle state */
|
|
22
|
+
@property({attribute: 'idle-text'}) idleText = 'Purchase';
|
|
23
|
+
|
|
24
|
+
/** The text to display in the processing state */
|
|
25
|
+
@property({attribute: 'processing-text'}) processingText = 'Purchasing';
|
|
26
|
+
|
|
27
|
+
/** The text to display in the success state */
|
|
28
|
+
@property({attribute: 'success-text'}) successText = 'Success';
|
|
29
|
+
|
|
30
|
+
/** The text to display in the failure state */
|
|
31
|
+
@property({attribute: 'failure-text'}) failureText = 'Failed';
|
|
32
|
+
|
|
33
|
+
/** The URL to redirect to after successful purchase */
|
|
34
|
+
@property({attribute: 'redirect-url'}) redirectUrl = '';
|
|
35
|
+
|
|
36
|
+
/** Delay in milliseconds before redirecting after success (default: 1500ms) */
|
|
37
|
+
@property({type: Number, attribute: 'redirect-delay'}) redirectDelay = 1500;
|
|
38
|
+
|
|
39
|
+
/** Delay in milliseconds before resetting from failure state (default: 2000ms) */
|
|
40
|
+
@property({type: Number, attribute: 'failure-reset-delay'}) failureResetDelay = 2000;
|
|
41
|
+
|
|
42
|
+
/** Disabled state */
|
|
43
|
+
@property({type: Boolean, reflect: true}) disabled = false;
|
|
44
|
+
|
|
45
|
+
/** The name of the form control (required for form submission) */
|
|
46
|
+
@property() name = '';
|
|
47
|
+
|
|
48
|
+
/** The value of the form control */
|
|
49
|
+
@property() value = '';
|
|
50
|
+
|
|
51
|
+
private resetTimeout?: number;
|
|
52
|
+
private redirectTimeout?: number;
|
|
53
|
+
|
|
54
|
+
disconnectedCallback(): void {
|
|
55
|
+
super.disconnectedCallback();
|
|
56
|
+
this.clearTimeouts();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private clearTimeouts() {
|
|
60
|
+
if (this.resetTimeout) {
|
|
61
|
+
clearTimeout(this.resetTimeout);
|
|
62
|
+
this.resetTimeout = undefined;
|
|
63
|
+
}
|
|
64
|
+
if (this.redirectTimeout) {
|
|
65
|
+
clearTimeout(this.redirectTimeout);
|
|
66
|
+
this.redirectTimeout = undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Programmatically trigger the purchase flow */
|
|
71
|
+
public purchase(): void {
|
|
72
|
+
if (this.currentState !== 'idle' || this.disabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.handlePurchase();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Set the button to success state manually */
|
|
80
|
+
public setSuccess(): void {
|
|
81
|
+
this.clearTimeouts();
|
|
82
|
+
this.currentState = 'success';
|
|
83
|
+
this.scheduleRedirect();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Set the button to failure state manually with optional error message */
|
|
87
|
+
public setFailure(message = ''): void {
|
|
88
|
+
this.clearTimeouts();
|
|
89
|
+
this.errorMessage = message;
|
|
90
|
+
this.currentState = 'failure';
|
|
91
|
+
this.scheduleReset();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Reset the button to idle state */
|
|
95
|
+
public reset(): void {
|
|
96
|
+
this.clearTimeouts();
|
|
97
|
+
this.currentState = 'idle';
|
|
98
|
+
this.errorMessage = '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handlePurchase() {
|
|
102
|
+
this.clearTimeouts();
|
|
103
|
+
this.currentState = 'processing';
|
|
104
|
+
|
|
105
|
+
const event = this.emit('zn-purchase', {
|
|
106
|
+
cancelable: true,
|
|
107
|
+
detail: {
|
|
108
|
+
value: this.value,
|
|
109
|
+
setSuccess: () => this.setSuccess(),
|
|
110
|
+
setFailure: (message?: string) => this.setFailure(message)
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// If the event was prevented, reset to idle
|
|
115
|
+
if (event.defaultPrevented) {
|
|
116
|
+
this.currentState = 'idle';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private scheduleRedirect() {
|
|
121
|
+
this.redirectTimeout = window.setTimeout(() => {
|
|
122
|
+
if (this.redirectUrl) {
|
|
123
|
+
window.location.href = this.redirectUrl;
|
|
124
|
+
}
|
|
125
|
+
this.emit('zn-redirect', {
|
|
126
|
+
detail: {url: this.redirectUrl}
|
|
127
|
+
});
|
|
128
|
+
}, this.redirectDelay);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private scheduleReset() {
|
|
132
|
+
this.resetTimeout = window.setTimeout(() => {
|
|
133
|
+
this.reset();
|
|
134
|
+
}, this.failureResetDelay);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private handleClick() {
|
|
138
|
+
if (this.currentState === 'idle' && !this.disabled) {
|
|
139
|
+
this.handlePurchase();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected render() {
|
|
144
|
+
const isDisabled = this.disabled || this.currentState !== 'idle';
|
|
145
|
+
const showSpinner = this.currentState === 'processing';
|
|
146
|
+
const showSuccess = this.currentState === 'success';
|
|
147
|
+
const showFailure = this.currentState === 'failure';
|
|
148
|
+
|
|
149
|
+
const classes = {
|
|
150
|
+
'animated-button': true,
|
|
151
|
+
'animated-button--idle': this.currentState === 'idle',
|
|
152
|
+
'animated-button--processing': this.currentState === 'processing',
|
|
153
|
+
'animated-button--success': this.currentState === 'success',
|
|
154
|
+
'animated-button--failure': this.currentState === 'failure'
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let buttonText = this.idleText;
|
|
158
|
+
if (this.currentState === 'processing') buttonText = this.processingText;
|
|
159
|
+
if (this.currentState === 'success') buttonText = this.successText;
|
|
160
|
+
if (this.currentState === 'failure') buttonText = this.failureText;
|
|
161
|
+
|
|
162
|
+
return html`
|
|
163
|
+
<button
|
|
164
|
+
class=${classMap(classes)}
|
|
165
|
+
?disabled=${isDisabled}
|
|
166
|
+
@click=${this.handleClick}
|
|
167
|
+
type="button"
|
|
168
|
+
aria-live="polite"
|
|
169
|
+
aria-busy=${showSpinner}
|
|
170
|
+
>
|
|
171
|
+
${showSpinner
|
|
172
|
+
? html`
|
|
173
|
+
<span class="animated-button__content">
|
|
174
|
+
<zn-icon
|
|
175
|
+
class="animated-button__spinner"
|
|
176
|
+
src="progress_activity"
|
|
177
|
+
size="20"
|
|
178
|
+
color="white"
|
|
179
|
+
></zn-icon>
|
|
180
|
+
<span class="animated-button__text">${buttonText}</span>
|
|
181
|
+
</span>
|
|
182
|
+
<span class="animated-button__shimmer"></span>
|
|
183
|
+
`
|
|
184
|
+
: showSuccess
|
|
185
|
+
? html`
|
|
186
|
+
<span class="animated-button__content">
|
|
187
|
+
<svg
|
|
188
|
+
class="animated-button__icon animated-button__icon--success"
|
|
189
|
+
width="20"
|
|
190
|
+
height="20"
|
|
191
|
+
viewBox="0 0 24 24"
|
|
192
|
+
fill="none"
|
|
193
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
194
|
+
>
|
|
195
|
+
<path
|
|
196
|
+
class="animated-button__tick"
|
|
197
|
+
d="M5 13l4 4L19 7"
|
|
198
|
+
stroke="white"
|
|
199
|
+
stroke-width="3"
|
|
200
|
+
stroke-linecap="round"
|
|
201
|
+
stroke-linejoin="round"
|
|
202
|
+
/>
|
|
203
|
+
</svg>
|
|
204
|
+
<span class="animated-button__text">${buttonText}</span>
|
|
205
|
+
</span>
|
|
206
|
+
`
|
|
207
|
+
: showFailure
|
|
208
|
+
? html`
|
|
209
|
+
<span class="animated-button__content">
|
|
210
|
+
<zn-icon
|
|
211
|
+
class="animated-button__icon animated-button__icon--failure"
|
|
212
|
+
src="cancel"
|
|
213
|
+
size="20"
|
|
214
|
+
color="white"
|
|
215
|
+
></zn-icon>
|
|
216
|
+
<span class="animated-button__text">
|
|
217
|
+
${buttonText}${this.errorMessage ? `: ${this.errorMessage}` : ''}
|
|
218
|
+
</span>
|
|
219
|
+
</span>
|
|
220
|
+
`
|
|
221
|
+
: html`
|
|
222
|
+
<span class="animated-button__content">
|
|
223
|
+
<span class="animated-button__text">${buttonText}</span>
|
|
224
|
+
</span>
|
|
225
|
+
`}
|
|
226
|
+
</button>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
}
|