@easemate/web-kit 0.1.5 → 0.2.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/README.md +360 -168
- package/build/elements/index.cjs +5 -2
- package/build/elements/index.d.cts +2 -1
- package/build/elements/index.d.ts +2 -1
- package/build/elements/index.js +2 -1
- package/build/elements/panel/index.cjs +496 -0
- package/build/elements/panel/index.d.cts +67 -0
- package/build/elements/panel/index.d.ts +67 -0
- package/build/elements/panel/index.js +492 -0
- package/build/elements/state/index.cjs +57 -464
- package/build/elements/state/index.d.cts +34 -25
- package/build/elements/state/index.d.ts +34 -25
- package/build/elements/state/index.js +59 -466
- package/build/internal/component-loaders.cjs +2 -0
- package/build/internal/component-loaders.d.cts +2 -2
- package/build/internal/component-loaders.d.ts +2 -2
- package/build/internal/component-loaders.js +2 -0
- package/build/react/events.cjs +25 -0
- package/build/react/events.d.cts +39 -0
- package/build/react/events.d.ts +39 -0
- package/build/react/events.js +22 -0
- package/build/react/index.cjs +19 -0
- package/build/react/index.d.cts +13 -0
- package/build/react/index.d.ts +13 -0
- package/build/react/index.js +12 -0
- package/build/react/provider.cjs +134 -0
- package/build/react/provider.d.cts +81 -0
- package/build/react/provider.d.ts +81 -0
- package/build/react/provider.js +98 -0
- package/build/react/types.cjs +8 -0
- package/build/react/types.d.cts +55 -0
- package/build/react/types.d.ts +55 -0
- package/build/react/types.js +7 -0
- package/build/react/use-ease-state.cjs +129 -0
- package/build/react/use-ease-state.d.cts +95 -0
- package/build/react/use-ease-state.d.ts +95 -0
- package/build/react/use-ease-state.js +126 -0
- package/build/react/use-web-kit.cjs +150 -0
- package/build/react/use-web-kit.d.cts +80 -0
- package/build/react/use-web-kit.d.ts +80 -0
- package/build/react/use-web-kit.js +114 -0
- package/build/register.cjs +1 -0
- package/build/register.d.cts +1 -0
- package/build/register.d.ts +1 -0
- package/build/register.js +1 -0
- package/package.json +15 -1
|
@@ -18,27 +18,45 @@ export interface StateChangeEventDetail {
|
|
|
18
18
|
event: Event;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* State aggregator component - collects and manages state from child controls.
|
|
22
|
+
*
|
|
23
|
+
* This component provides state management without any visual styling.
|
|
24
|
+
* Use it standalone or wrap it with `<ease-panel>` for a styled container.
|
|
25
|
+
*
|
|
26
|
+
* @tag ease-state
|
|
27
|
+
*
|
|
28
|
+
* @slot - Default slot for controls
|
|
29
|
+
*
|
|
30
|
+
* @fires state-change - Fired when any control value changes
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```html
|
|
34
|
+
* <!-- Standalone usage (no panel) -->
|
|
35
|
+
* <ease-state>
|
|
36
|
+
* <ease-field label="Duration">
|
|
37
|
+
* <ease-slider name="duration" value="1" min="0" max="5"></ease-slider>
|
|
38
|
+
* </ease-field>
|
|
39
|
+
* <ease-field label="Loop">
|
|
40
|
+
* <ease-toggle name="loop"></ease-toggle>
|
|
41
|
+
* </ease-field>
|
|
42
|
+
* </ease-state>
|
|
43
|
+
*
|
|
44
|
+
* <!-- With panel wrapper -->
|
|
45
|
+
* <ease-panel>
|
|
46
|
+
* <span slot="headline">Animation Controls</span>
|
|
47
|
+
* <ease-state>
|
|
48
|
+
* <ease-field label="Duration">
|
|
49
|
+
* <ease-slider name="duration" value="1" min="0" max="5"></ease-slider>
|
|
50
|
+
* </ease-field>
|
|
51
|
+
* </ease-state>
|
|
52
|
+
* </ease-panel>
|
|
53
|
+
* ```
|
|
22
54
|
*/
|
|
23
|
-
export interface TabChangeEventDetail {
|
|
24
|
-
/** The index of the active tab */
|
|
25
|
-
index: number;
|
|
26
|
-
/** The tab id */
|
|
27
|
-
id: string;
|
|
28
|
-
/** The original event */
|
|
29
|
-
event: Event;
|
|
30
|
-
}
|
|
31
55
|
export declare class State extends HTMLElement {
|
|
32
56
|
#private;
|
|
33
57
|
requestRender: () => void;
|
|
34
58
|
accessor value: string | null;
|
|
35
|
-
accessor
|
|
36
|
-
/** @internal */
|
|
37
|
-
handleActiveTabChange(previous: number, next: number): void;
|
|
38
|
-
accessor entrySlot: HTMLSlotElement | null;
|
|
39
|
-
accessor outputElement: HTMLOutputElement | null;
|
|
40
|
-
accessor contentElement: HTMLElement | null;
|
|
41
|
-
accessor formElement: HTMLElement | null;
|
|
59
|
+
accessor defaultSlot: HTMLSlotElement | null;
|
|
42
60
|
/**
|
|
43
61
|
* Get the current state object with all control values
|
|
44
62
|
*/
|
|
@@ -67,20 +85,11 @@ export declare class State extends HTMLElement {
|
|
|
67
85
|
* Reset all controls to their initial values
|
|
68
86
|
*/
|
|
69
87
|
reset(): void;
|
|
70
|
-
/**
|
|
71
|
-
* Switch to a specific tab by index
|
|
72
|
-
* @param index - The tab index (0-based)
|
|
73
|
-
*/
|
|
74
|
-
setTab(index: number): void;
|
|
75
88
|
connectedCallback(): void;
|
|
76
89
|
disconnectedCallback(): void;
|
|
77
|
-
afterRender(): void;
|
|
78
90
|
render(): TemplateResult;
|
|
79
|
-
performTabAnimation(fromIndex: number, toIndex: number): Promise<void>;
|
|
80
91
|
handleInternalInput(event: CustomEvent<ControlEventDetail>): void;
|
|
81
92
|
handleInternalChange(event: CustomEvent<ControlEventDetail>): void;
|
|
82
93
|
handleControlChange(event: CustomEvent<ControlEventDetail>): void;
|
|
83
|
-
onFooterSlotChange(): void;
|
|
84
|
-
private updateFooterAttribute;
|
|
85
94
|
}
|
|
86
95
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { html
|
|
2
|
-
import { CONTROL_CHANGE_EVENT, dispatchControlEvent
|
|
1
|
+
import { html } from 'lit-html';
|
|
2
|
+
import { CONTROL_CHANGE_EVENT, dispatchControlEvent } from "../shared.js";
|
|
3
3
|
import { Component } from '../../decorators/Component';
|
|
4
4
|
import { Listen } from '../../decorators/Listen';
|
|
5
5
|
import { Prop } from '../../decorators/Prop';
|
|
@@ -25,195 +25,52 @@ const getControlName = (element) => {
|
|
|
25
25
|
}
|
|
26
26
|
return element.getAttribute?.('name') ?? null;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* State aggregator component - collects and manages state from child controls.
|
|
30
|
+
*
|
|
31
|
+
* This component provides state management without any visual styling.
|
|
32
|
+
* Use it standalone or wrap it with `<ease-panel>` for a styled container.
|
|
33
|
+
*
|
|
34
|
+
* @tag ease-state
|
|
35
|
+
*
|
|
36
|
+
* @slot - Default slot for controls
|
|
37
|
+
*
|
|
38
|
+
* @fires state-change - Fired when any control value changes
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```html
|
|
42
|
+
* <!-- Standalone usage (no panel) -->
|
|
43
|
+
* <ease-state>
|
|
44
|
+
* <ease-field label="Duration">
|
|
45
|
+
* <ease-slider name="duration" value="1" min="0" max="5"></ease-slider>
|
|
46
|
+
* </ease-field>
|
|
47
|
+
* <ease-field label="Loop">
|
|
48
|
+
* <ease-toggle name="loop"></ease-toggle>
|
|
49
|
+
* </ease-field>
|
|
50
|
+
* </ease-state>
|
|
51
|
+
*
|
|
52
|
+
* <!-- With panel wrapper -->
|
|
53
|
+
* <ease-panel>
|
|
54
|
+
* <span slot="headline">Animation Controls</span>
|
|
55
|
+
* <ease-state>
|
|
56
|
+
* <ease-field label="Duration">
|
|
57
|
+
* <ease-slider name="duration" value="1" min="0" max="5"></ease-slider>
|
|
58
|
+
* </ease-field>
|
|
59
|
+
* </ease-state>
|
|
60
|
+
* </ease-panel>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
28
63
|
@Component({
|
|
29
64
|
tag: 'ease-state',
|
|
30
65
|
shadowMode: 'open',
|
|
31
66
|
styles: `
|
|
32
67
|
:host {
|
|
33
|
-
|
|
34
|
-
--ease-state-transition-easing: cubic-bezier(.25, 0, .5, 1);
|
|
68
|
+
display: contents;
|
|
35
69
|
}
|
|
36
70
|
|
|
37
|
-
[part="
|
|
38
|
-
display: block;
|
|
39
|
-
width: 100%;
|
|
40
|
-
max-width: var(--ease-panel-max-width, 332px);
|
|
41
|
-
border-radius: var(--ease-panel-radius, 12px);
|
|
42
|
-
border: 1px solid var(--ease-panel-border-color, var(--color-white-6));
|
|
43
|
-
background-clip: padding-box;
|
|
44
|
-
background: var(--ease-panel-background, var(--color-gray-1000));
|
|
45
|
-
box-shadow: var(--ease-panel-shadow, 0 0 40px 0 var(--color-white-2) inset);
|
|
46
|
-
box-sizing: border-box;
|
|
47
|
-
padding: var(--ease-panel-padding, 12px);
|
|
48
|
-
margin: auto;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
[part="header"] {
|
|
52
|
-
display: flex;
|
|
53
|
-
align-items: center;
|
|
54
|
-
gap: 8px;
|
|
55
|
-
width: 100%;
|
|
56
|
-
margin-bottom: 12px;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
[part="headline"] {
|
|
60
|
-
font-size: var(--ease-panel-title-font-size, 14px);
|
|
61
|
-
font-weight: var(--ease-panel-title-font-weight, 500);
|
|
62
|
-
line-height: var(--ease-panel-title-line-height, 24px);
|
|
63
|
-
font-family: var(--ease-font-family, "Instrument Sans", sans-serif);
|
|
64
|
-
color: var(--ease-panel-title-color, var(--color-blue-100));
|
|
65
|
-
margin: 0 0 0 4px;
|
|
66
|
-
flex-grow: 1;
|
|
67
|
-
text-ellipsis: ellipsis;
|
|
68
|
-
overflow: hidden;
|
|
69
|
-
white-space: nowrap;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
[part="headline"]:has(+ [part="tabs"]:not(:empty)) {
|
|
73
|
-
display: none;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
[part="tabs"] {
|
|
77
|
-
display: flex;
|
|
78
|
-
align-items: center;
|
|
79
|
-
gap: 2px;
|
|
80
|
-
flex-grow: 1;
|
|
81
|
-
margin: 0 0 0 4px;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
[part="tabs"]:empty {
|
|
85
|
-
display: none;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
[part="tab"] {
|
|
89
|
-
appearance: none;
|
|
90
|
-
font-size: var(--ease-panel-tab-font-size, 13px);
|
|
91
|
-
font-weight: var(--ease-panel-tab-font-weight, 500);
|
|
92
|
-
line-height: var(--ease-panel-tab-line-height, 24px);
|
|
93
|
-
font-family: var(--ease-font-family, "Instrument Sans", sans-serif);
|
|
94
|
-
color: var(--ease-panel-tab-color, var(--color-gray-600));
|
|
95
|
-
background: transparent;
|
|
96
|
-
border: none;
|
|
97
|
-
padding: 4px 8px;
|
|
98
|
-
margin: 0;
|
|
99
|
-
cursor: pointer;
|
|
100
|
-
border-radius: var(--ease-panel-tab-radius, 6px);
|
|
101
|
-
transition: color 0.2s, background-color 0.2s;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
[part="tab"]:hover {
|
|
105
|
-
color: var(--ease-panel-tab-color-hover, var(--color-blue-100));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
[part="tab"][aria-selected="true"] {
|
|
109
|
-
color: var(--ease-panel-tab-color-active, var(--color-blue-100));
|
|
110
|
-
background: var(--ease-panel-tab-background-active, var(--color-white-4));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
[part="actions"] {
|
|
114
|
-
display: flex;
|
|
115
|
-
align-items: center;
|
|
116
|
-
gap: 4px;
|
|
117
|
-
margin-left: auto;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
slot[name="actions"]::slotted(button),
|
|
121
|
-
slot[name="actions"]::slotted(a) {
|
|
122
|
-
--ease-icon-size: var(--ease-panel-action-icon-size, 16px);
|
|
123
|
-
|
|
124
|
-
appearance: none;
|
|
125
|
-
flex: 0 0 24px;
|
|
126
|
-
border: none;
|
|
127
|
-
outline: none;
|
|
128
|
-
background-color: transparent;
|
|
129
|
-
padding: 4px;
|
|
130
|
-
margin: 0;
|
|
131
|
-
cursor: pointer;
|
|
132
|
-
color: var(--color-gray-600);
|
|
133
|
-
transition: color 0.2s;
|
|
134
|
-
text-decoration: none;
|
|
135
|
-
display: flex;
|
|
136
|
-
align-items: center;
|
|
137
|
-
justify-content: center;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
slot[name="actions"]::slotted(button:hover),
|
|
141
|
-
slot[name="actions"]::slotted(button:focus-visible),
|
|
142
|
-
slot[name="actions"]::slotted(a:hover),
|
|
143
|
-
slot[name="actions"]::slotted(a:focus-visible) {
|
|
144
|
-
color: var(--color-blue-100);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
slot[name="actions"]::slotted(ease-dropdown) {
|
|
148
|
-
flex: 0 0 auto;
|
|
149
|
-
width: auto;
|
|
150
|
-
|
|
151
|
-
--ease-icon-size: var(--ease-panel-action-icon-size, 16px);
|
|
152
|
-
--ease-dropdown-trigger-padding: 4px;
|
|
153
|
-
--ease-dropdown-radius: 6px;
|
|
154
|
-
--ease-dropdown-background: transparent;
|
|
155
|
-
--ease-dropdown-background-hover: transparent;
|
|
156
|
-
--ease-dropdown-shadow: none;
|
|
157
|
-
--ease-dropdown-color: var(--color-gray-600);
|
|
158
|
-
--ease-popover-placement: bottom-end;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
slot[name="actions"]::slotted(ease-dropdown:hover),
|
|
162
|
-
slot[name="actions"]::slotted(ease-dropdown:focus-within) {
|
|
163
|
-
--ease-dropdown-color: var(--color-blue-100);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
[part="content"] {
|
|
167
|
-
display: block;
|
|
168
|
-
width: 100%;
|
|
169
|
-
box-sizing: border-box;
|
|
170
|
-
margin: auto;
|
|
171
|
-
overflow: hidden;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
[part="content"][data-animating="true"] {
|
|
175
|
-
transition: height var(--ease-state-transition-duration) var(--ease-state-transition-easing);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
[part="form"] {
|
|
179
|
-
width: 100%;
|
|
180
|
-
position: relative;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
[part="tab-panel"] {
|
|
184
|
-
width: 100%;
|
|
185
|
-
pointer-events: none;
|
|
186
|
-
display: none;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
[part="tab-panel"][data-state="active"] {
|
|
190
|
-
display: block;
|
|
191
|
-
pointer-events: auto;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
[part="tab-panel"][data-state="hidden"] {
|
|
195
|
-
display: none;
|
|
196
|
-
pointer-events: none;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
[part="footer"] {
|
|
200
|
-
display: flex;
|
|
201
|
-
align-items: center;
|
|
202
|
-
justify-content: space-between;
|
|
203
|
-
width: 100%;
|
|
204
|
-
padding: var(--ease-panel-footer-padding, 12px);
|
|
205
|
-
box-sizing: border-box;
|
|
206
|
-
border-top: 1px solid var(--color-white-4);
|
|
207
|
-
|
|
208
|
-
&:not(:has([data-has-content="true"])) {
|
|
209
|
-
display: none;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
::slotted([slot="entry"]),
|
|
214
|
-
::slotted([slot^="tab-"]) {
|
|
71
|
+
[part="container"] {
|
|
215
72
|
display: grid;
|
|
216
|
-
gap: 12px;
|
|
73
|
+
gap: var(--ease-state-gap, 12px);
|
|
217
74
|
box-sizing: border-box;
|
|
218
75
|
width: 100%;
|
|
219
76
|
}
|
|
@@ -224,35 +81,10 @@ export class State extends HTMLElement {
|
|
|
224
81
|
#state = {};
|
|
225
82
|
#initialState = {};
|
|
226
83
|
#subscribers = new Map();
|
|
227
|
-
#tabs = [];
|
|
228
|
-
#isAnimating = false;
|
|
229
84
|
@Prop({ reflect: true })
|
|
230
85
|
accessor value;
|
|
231
|
-
@
|
|
232
|
-
|
|
233
|
-
reflect: true,
|
|
234
|
-
attribute: 'active-tab',
|
|
235
|
-
defaultValue: 0,
|
|
236
|
-
onChange(next, previous) {
|
|
237
|
-
const self = this;
|
|
238
|
-
if (next !== previous && previous !== undefined) {
|
|
239
|
-
self.handleActiveTabChange(previous, next);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
accessor activeTab = 0;
|
|
244
|
-
/** @internal */
|
|
245
|
-
handleActiveTabChange(previous, next) {
|
|
246
|
-
this.performTabAnimation(previous, next);
|
|
247
|
-
}
|
|
248
|
-
@Query('slot[name="entry"]')
|
|
249
|
-
accessor entrySlot;
|
|
250
|
-
@Query('output')
|
|
251
|
-
accessor outputElement;
|
|
252
|
-
@Query('[part="content"]')
|
|
253
|
-
accessor contentElement;
|
|
254
|
-
@Query('[part="form"]')
|
|
255
|
-
accessor formElement;
|
|
86
|
+
@Query('slot')
|
|
87
|
+
accessor defaultSlot;
|
|
256
88
|
/**
|
|
257
89
|
* Get the current state object with all control values
|
|
258
90
|
*/
|
|
@@ -317,233 +149,21 @@ export class State extends HTMLElement {
|
|
|
317
149
|
this.set(name, value);
|
|
318
150
|
}
|
|
319
151
|
}
|
|
320
|
-
/**
|
|
321
|
-
* Switch to a specific tab by index
|
|
322
|
-
* @param index - The tab index (0-based)
|
|
323
|
-
*/
|
|
324
|
-
setTab(index) {
|
|
325
|
-
if (index >= 0 && index < this.#tabs.length && index !== this.activeTab) {
|
|
326
|
-
this.activeTab = index;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
152
|
connectedCallback() {
|
|
330
|
-
this.#syncTabs();
|
|
331
153
|
this.#attach();
|
|
332
|
-
this.
|
|
154
|
+
this.defaultSlot?.addEventListener('slotchange', this.#handleSlotChange);
|
|
333
155
|
}
|
|
334
156
|
disconnectedCallback() {
|
|
335
157
|
this.#detach();
|
|
336
|
-
this.
|
|
337
|
-
}
|
|
338
|
-
afterRender() {
|
|
339
|
-
if (this.outputElement) {
|
|
340
|
-
this.outputElement.value = this.value ?? '';
|
|
341
|
-
}
|
|
342
|
-
this.#syncTabs();
|
|
158
|
+
this.defaultSlot?.removeEventListener('slotchange', this.#handleSlotChange);
|
|
343
159
|
}
|
|
344
160
|
render() {
|
|
345
|
-
const hasTabs = this.#tabs.length > 0;
|
|
346
161
|
return html `
|
|
347
|
-
<
|
|
348
|
-
<
|
|
349
|
-
<h3 part="headline"><slot name="headline"></slot></h3>
|
|
350
|
-
${this.#renderTabs()}
|
|
351
|
-
<div part="actions">
|
|
352
|
-
<slot name="actions"></slot>
|
|
353
|
-
</div>
|
|
354
|
-
</div>
|
|
355
|
-
<div part="content">
|
|
356
|
-
<div part="form">
|
|
357
|
-
${hasTabs ? this.#renderTabPanels() : html `<slot name="entry"></slot>`}
|
|
358
|
-
</div>
|
|
359
|
-
</div>
|
|
360
|
-
<div part="footer">
|
|
361
|
-
<slot name="footer"></slot>
|
|
362
|
-
</div>
|
|
363
|
-
</section>
|
|
364
|
-
`;
|
|
365
|
-
}
|
|
366
|
-
#renderTabs() {
|
|
367
|
-
if (this.#tabs.length === 0) {
|
|
368
|
-
return nothing;
|
|
369
|
-
}
|
|
370
|
-
return html `
|
|
371
|
-
<div part="tabs" role="tablist">
|
|
372
|
-
${this.#tabs.map((tab, index) => html `
|
|
373
|
-
<button
|
|
374
|
-
part="tab"
|
|
375
|
-
role="tab"
|
|
376
|
-
aria-selected=${index === this.activeTab ? 'true' : 'false'}
|
|
377
|
-
aria-controls=${`panel-${tab.id}`}
|
|
378
|
-
tabindex=${index === this.activeTab ? 0 : -1}
|
|
379
|
-
@click=${(e) => this.#handleTabClick(index, tab.id, e)}
|
|
380
|
-
@keydown=${(e) => this.#handleTabKeydown(e, index)}
|
|
381
|
-
>
|
|
382
|
-
${tab.label}
|
|
383
|
-
</button>
|
|
384
|
-
`)}
|
|
162
|
+
<div part="container">
|
|
163
|
+
<slot></slot>
|
|
385
164
|
</div>
|
|
386
165
|
`;
|
|
387
166
|
}
|
|
388
|
-
#renderTabPanels() {
|
|
389
|
-
return html `
|
|
390
|
-
${this.#tabs.map((tab, index) => html `
|
|
391
|
-
<div
|
|
392
|
-
part="tab-panel"
|
|
393
|
-
role="tabpanel"
|
|
394
|
-
id=${`panel-${tab.id}`}
|
|
395
|
-
aria-labelledby=${`tab-${tab.id}`}
|
|
396
|
-
data-state=${index === this.activeTab ? 'active' : 'hidden'}
|
|
397
|
-
data-index=${index}
|
|
398
|
-
>
|
|
399
|
-
<slot name=${`tab-${tab.id}`}></slot>
|
|
400
|
-
</div>
|
|
401
|
-
`)}
|
|
402
|
-
`;
|
|
403
|
-
}
|
|
404
|
-
#handleTabClick(index, id, event) {
|
|
405
|
-
if (index === this.activeTab) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
this.activeTab = index;
|
|
409
|
-
dispatchControlEvent(this, 'tab-change', {
|
|
410
|
-
index,
|
|
411
|
-
id,
|
|
412
|
-
event
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
#handleTabKeydown(event, currentIndex) {
|
|
416
|
-
let newIndex = currentIndex;
|
|
417
|
-
switch (event.key) {
|
|
418
|
-
case 'ArrowLeft':
|
|
419
|
-
event.preventDefault();
|
|
420
|
-
newIndex = currentIndex > 0 ? currentIndex - 1 : this.#tabs.length - 1;
|
|
421
|
-
break;
|
|
422
|
-
case 'ArrowRight':
|
|
423
|
-
event.preventDefault();
|
|
424
|
-
newIndex = currentIndex < this.#tabs.length - 1 ? currentIndex + 1 : 0;
|
|
425
|
-
break;
|
|
426
|
-
case 'Home':
|
|
427
|
-
event.preventDefault();
|
|
428
|
-
newIndex = 0;
|
|
429
|
-
break;
|
|
430
|
-
case 'End':
|
|
431
|
-
event.preventDefault();
|
|
432
|
-
newIndex = this.#tabs.length - 1;
|
|
433
|
-
break;
|
|
434
|
-
default:
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (newIndex !== currentIndex) {
|
|
438
|
-
this.activeTab = newIndex;
|
|
439
|
-
// Focus the new tab button
|
|
440
|
-
queueMicrotask(() => {
|
|
441
|
-
const tabButtons = this.shadowRoot?.querySelectorAll('[part="tab"]');
|
|
442
|
-
const newTabButton = tabButtons?.[newIndex];
|
|
443
|
-
newTabButton?.focus();
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async performTabAnimation(fromIndex, toIndex) {
|
|
448
|
-
if (this.#isAnimating) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
this.#isAnimating = true;
|
|
452
|
-
const duration = 120;
|
|
453
|
-
const easing = 'cubic-bezier(.25, 0, .5, 1)';
|
|
454
|
-
const content = this.contentElement;
|
|
455
|
-
if (!content) {
|
|
456
|
-
this.#isAnimating = false;
|
|
457
|
-
this.requestRender();
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
// Get the panels by data-index attribute for reliability
|
|
461
|
-
const fromPanel = this.shadowRoot?.querySelector(`[part="tab-panel"][data-index="${fromIndex}"]`);
|
|
462
|
-
const toPanel = this.shadowRoot?.querySelector(`[part="tab-panel"][data-index="${toIndex}"]`);
|
|
463
|
-
if (!fromPanel || !toPanel) {
|
|
464
|
-
this.#isAnimating = false;
|
|
465
|
-
this.requestRender();
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
// Lock the current height
|
|
469
|
-
const startHeight = content.getBoundingClientRect().height;
|
|
470
|
-
content.style.height = `${startHeight}px`;
|
|
471
|
-
// FIX: Ensure the new panel is hidden immediately.
|
|
472
|
-
// Changing activeTab triggers a render which sets data-state="active" (display: block).
|
|
473
|
-
// We must override this with inline styles to prevent the content from showing during the fade-out.
|
|
474
|
-
toPanel.style.display = 'none';
|
|
475
|
-
toPanel.style.opacity = '0';
|
|
476
|
-
// Fade out old content via WAAPI (avoids any "one-frame" flashes)
|
|
477
|
-
try {
|
|
478
|
-
const fadeOut = fromPanel.animate([{ opacity: 1 }, { opacity: 0 }], { duration, easing, fill: 'forwards' });
|
|
479
|
-
await fadeOut.finished;
|
|
480
|
-
fadeOut.cancel();
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
// ignore
|
|
484
|
-
}
|
|
485
|
-
fromPanel.setAttribute('data-state', 'hidden');
|
|
486
|
-
// Prepare and measure new panel while completely invisible
|
|
487
|
-
const previousToState = toPanel.getAttribute('data-state');
|
|
488
|
-
// Reset display to block (overriding our 'none' above) but keep invisible for measuring
|
|
489
|
-
toPanel.style.display = 'block';
|
|
490
|
-
toPanel.style.visibility = 'hidden';
|
|
491
|
-
toPanel.style.opacity = '0';
|
|
492
|
-
// Force layout, then measure
|
|
493
|
-
void toPanel.offsetHeight;
|
|
494
|
-
const endHeight = toPanel.getBoundingClientRect().height;
|
|
495
|
-
// Animate height
|
|
496
|
-
if (startHeight !== endHeight) {
|
|
497
|
-
content.setAttribute('data-animating', 'true');
|
|
498
|
-
void content.offsetHeight;
|
|
499
|
-
content.style.height = `${endHeight}px`;
|
|
500
|
-
await this.#wait(duration);
|
|
501
|
-
}
|
|
502
|
-
// Show panel but keep opacity at 0, then fade in
|
|
503
|
-
toPanel.style.visibility = 'visible';
|
|
504
|
-
toPanel.style.opacity = '0';
|
|
505
|
-
// Ensure the 0-opacity state is committed
|
|
506
|
-
void toPanel.offsetHeight;
|
|
507
|
-
try {
|
|
508
|
-
const fadeIn = toPanel.animate([{ opacity: 0 }, { opacity: 1 }], { duration, easing, fill: 'forwards' });
|
|
509
|
-
await fadeIn.finished;
|
|
510
|
-
fadeIn.cancel();
|
|
511
|
-
}
|
|
512
|
-
catch {
|
|
513
|
-
// ignore
|
|
514
|
-
}
|
|
515
|
-
// Finalize new tab state and cleanup
|
|
516
|
-
toPanel.style.display = '';
|
|
517
|
-
toPanel.style.visibility = '';
|
|
518
|
-
toPanel.style.opacity = '';
|
|
519
|
-
// Restore state attribute (we only want one active)
|
|
520
|
-
if (previousToState !== 'active') {
|
|
521
|
-
toPanel.setAttribute('data-state', 'active');
|
|
522
|
-
}
|
|
523
|
-
content.style.height = '';
|
|
524
|
-
content.removeAttribute('data-animating');
|
|
525
|
-
this.#isAnimating = false;
|
|
526
|
-
this.#detach();
|
|
527
|
-
this.#attach();
|
|
528
|
-
}
|
|
529
|
-
#wait(ms) {
|
|
530
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
531
|
-
}
|
|
532
|
-
#syncTabs() {
|
|
533
|
-
const tabs = [];
|
|
534
|
-
for (const child of Array.from(this.children)) {
|
|
535
|
-
const slot = child.getAttribute('slot');
|
|
536
|
-
if (slot?.startsWith('tab-')) {
|
|
537
|
-
const id = slot.replace('tab-', '');
|
|
538
|
-
const label = child.getAttribute('data-tab-label') || id;
|
|
539
|
-
tabs.push({ id, label });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
this.#tabs = tabs.slice(0, 3);
|
|
543
|
-
if (this.activeTab >= this.#tabs.length && this.#tabs.length > 0) {
|
|
544
|
-
this.activeTab = 0;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
167
|
@Listen('input', { target: (host) => host })
|
|
548
168
|
handleInternalInput(event) {
|
|
549
169
|
this.#handleControlEvent(event);
|
|
@@ -556,10 +176,6 @@ export class State extends HTMLElement {
|
|
|
556
176
|
handleControlChange(event) {
|
|
557
177
|
this.#handleControlEvent(event);
|
|
558
178
|
}
|
|
559
|
-
@Listen('slotchange', { selector: 'slot[name="footer"]' })
|
|
560
|
-
onFooterSlotChange() {
|
|
561
|
-
this.updateFooterAttribute();
|
|
562
|
-
}
|
|
563
179
|
#handleControlEvent(event) {
|
|
564
180
|
if ('detail' in event && event.detail?.name) {
|
|
565
181
|
this.#updateState(event.detail.name, event.detail.value, event);
|
|
@@ -577,25 +193,13 @@ export class State extends HTMLElement {
|
|
|
577
193
|
this.#updateState(name, value, event);
|
|
578
194
|
}
|
|
579
195
|
#handleSlotChange = () => {
|
|
580
|
-
this.#syncTabs();
|
|
581
196
|
this.#detach();
|
|
582
197
|
this.#attach();
|
|
583
198
|
};
|
|
584
199
|
#attach() {
|
|
585
|
-
const
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
slots.push(this.entrySlot);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
const activeTab = this.#tabs[this.activeTab];
|
|
593
|
-
if (activeTab) {
|
|
594
|
-
const tabSlot = this.shadowRoot?.querySelector(`slot[name="tab-${activeTab.id}"]`);
|
|
595
|
-
if (tabSlot) {
|
|
596
|
-
slots.push(tabSlot);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
200
|
+
const slot = this.defaultSlot;
|
|
201
|
+
if (!slot) {
|
|
202
|
+
return;
|
|
599
203
|
}
|
|
600
204
|
const findControls = (el) => {
|
|
601
205
|
const controls = [];
|
|
@@ -621,18 +225,16 @@ export class State extends HTMLElement {
|
|
|
621
225
|
};
|
|
622
226
|
this.#controls.clear();
|
|
623
227
|
this.#state = {};
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.#initialState[name] = value;
|
|
635
|
-
}
|
|
228
|
+
const elements = slot.assignedElements({ flatten: true });
|
|
229
|
+
for (const element of elements) {
|
|
230
|
+
const controls = findControls(element);
|
|
231
|
+
for (const control of controls) {
|
|
232
|
+
const name = getControlName(control);
|
|
233
|
+
if (name) {
|
|
234
|
+
this.#controls.set(name, control);
|
|
235
|
+
const value = readControlValue(control);
|
|
236
|
+
this.#state[name] = value;
|
|
237
|
+
this.#initialState[name] = value;
|
|
636
238
|
}
|
|
637
239
|
}
|
|
638
240
|
}
|
|
@@ -665,13 +267,4 @@ export class State extends HTMLElement {
|
|
|
665
267
|
event
|
|
666
268
|
});
|
|
667
269
|
}
|
|
668
|
-
updateFooterAttribute() {
|
|
669
|
-
const footer = this.shadowRoot?.querySelector('[part="footer"]');
|
|
670
|
-
if (!footer) {
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
const footerSlot = this.shadowRoot?.querySelector('slot[name="footer"]');
|
|
674
|
-
const hasFooter = Boolean(footerSlot?.assignedNodes({ flatten: true }).length > 0);
|
|
675
|
-
setBooleanAttribute(footer, 'data-has-content', hasFooter);
|
|
676
|
-
}
|
|
677
270
|
}
|
|
@@ -60,6 +60,7 @@ exports.WEB_KIT_ELEMENT_TAGS = [
|
|
|
60
60
|
'ease-toggle',
|
|
61
61
|
// Layout
|
|
62
62
|
'ease-field',
|
|
63
|
+
'ease-panel',
|
|
63
64
|
'ease-popover',
|
|
64
65
|
'ease-state',
|
|
65
66
|
'ease-tooltip',
|
|
@@ -132,6 +133,7 @@ exports.COMPONENT_LOADERS = {
|
|
|
132
133
|
'ease-toggle': () => Promise.resolve().then(() => __importStar(require("../elements/toggle/index.cjs"))),
|
|
133
134
|
// Layout
|
|
134
135
|
'ease-field': () => Promise.resolve().then(() => __importStar(require("../elements/field/index.cjs"))),
|
|
136
|
+
'ease-panel': () => Promise.resolve().then(() => __importStar(require("../elements/panel/index.cjs"))),
|
|
135
137
|
'ease-popover': () => Promise.resolve().then(() => __importStar(require("../elements/popover/index.cjs"))),
|
|
136
138
|
'ease-state': () => Promise.resolve().then(() => __importStar(require("../elements/state/index.cjs"))),
|
|
137
139
|
'ease-tooltip': () => Promise.resolve().then(() => __importStar(require("../elements/tooltip/index.cjs"))),
|