@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
package/build/elements/index.cjs
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.Tooltip = exports.Toggle = exports.Slider = exports.setBooleanAttribute = exports.readControlValue = exports.dispatchControlEvent = exports.coerceNumber = exports.CONTROL_CHANGE_EVENT = exports.RadioOption = exports.RadioInput = exports.RadioGroup = exports.Popover = exports.Origin = exports.NumberInput = exports.MonitorFps = exports.Monitor = exports.Input = exports.Field = exports.Dropdown = exports.ColorPicker = exports.ColorInput = exports.Checkbox = exports.Button = void 0;
|
|
17
|
+
exports.Tooltip = exports.Toggle = exports.State = exports.Slider = exports.setBooleanAttribute = exports.readControlValue = exports.dispatchControlEvent = exports.coerceNumber = exports.CONTROL_CHANGE_EVENT = exports.RadioOption = exports.RadioInput = exports.RadioGroup = exports.Popover = exports.Panel = exports.Origin = exports.NumberInput = exports.MonitorFps = exports.Monitor = exports.Input = exports.Field = exports.Dropdown = exports.ColorPicker = exports.ColorInput = exports.Checkbox = exports.Button = void 0;
|
|
18
18
|
var button_1 = require("./button/index.cjs");
|
|
19
19
|
Object.defineProperty(exports, "Button", { enumerable: true, get: function () { return button_1.Button; } });
|
|
20
20
|
var checkbox_1 = require("./checkbox/index.cjs");
|
|
@@ -39,6 +39,8 @@ var number_1 = require("./number/index.cjs");
|
|
|
39
39
|
Object.defineProperty(exports, "NumberInput", { enumerable: true, get: function () { return number_1.NumberInput; } });
|
|
40
40
|
var origin_1 = require("./origin/index.cjs");
|
|
41
41
|
Object.defineProperty(exports, "Origin", { enumerable: true, get: function () { return origin_1.Origin; } });
|
|
42
|
+
var panel_1 = require("./panel/index.cjs");
|
|
43
|
+
Object.defineProperty(exports, "Panel", { enumerable: true, get: function () { return panel_1.Panel; } });
|
|
42
44
|
var popover_1 = require("./popover/index.cjs");
|
|
43
45
|
Object.defineProperty(exports, "Popover", { enumerable: true, get: function () { return popover_1.Popover; } });
|
|
44
46
|
var radio_1 = require("./radio/index.cjs");
|
|
@@ -55,7 +57,8 @@ Object.defineProperty(exports, "readControlValue", { enumerable: true, get: func
|
|
|
55
57
|
Object.defineProperty(exports, "setBooleanAttribute", { enumerable: true, get: function () { return shared_1.setBooleanAttribute; } });
|
|
56
58
|
var slider_1 = require("./slider/index.cjs");
|
|
57
59
|
Object.defineProperty(exports, "Slider", { enumerable: true, get: function () { return slider_1.Slider; } });
|
|
58
|
-
|
|
60
|
+
var state_1 = require("./state/index.cjs");
|
|
61
|
+
Object.defineProperty(exports, "State", { enumerable: true, get: function () { return state_1.State; } });
|
|
59
62
|
var toggle_1 = require("./toggle/index.cjs");
|
|
60
63
|
Object.defineProperty(exports, "Toggle", { enumerable: true, get: function () { return toggle_1.Toggle; } });
|
|
61
64
|
var tooltip_1 = require("./tooltip/index.cjs");
|
|
@@ -11,12 +11,13 @@ export { Monitor } from "./monitor/index.cjs";
|
|
|
11
11
|
export { MonitorFps } from "./monitor/fps.cjs";
|
|
12
12
|
export { NumberInput } from "./number/index.cjs";
|
|
13
13
|
export { Origin } from "./origin/index.cjs";
|
|
14
|
+
export { Panel, type TabChangeEventDetail } from "./panel/index.cjs";
|
|
14
15
|
export { type Placement, Popover } from "./popover/index.cjs";
|
|
15
16
|
export { RadioGroup } from "./radio/index.cjs";
|
|
16
17
|
export { RadioInput } from "./radio/input.cjs";
|
|
17
18
|
export { RadioOption } from "./radio/option.cjs";
|
|
18
19
|
export { CONTROL_CHANGE_EVENT, type ControlEventDetail, coerceNumber, dispatchControlEvent, readControlValue, setBooleanAttribute } from "./shared.cjs";
|
|
19
20
|
export { Slider } from "./slider/index.cjs";
|
|
20
|
-
export
|
|
21
|
+
export { State, type StateChangeEventDetail } from "./state/index.cjs";
|
|
21
22
|
export { Toggle } from "./toggle/index.cjs";
|
|
22
23
|
export { Tooltip } from "./tooltip/index.cjs";
|
|
@@ -11,12 +11,13 @@ export { Monitor } from "./monitor/index.js";
|
|
|
11
11
|
export { MonitorFps } from "./monitor/fps.js";
|
|
12
12
|
export { NumberInput } from "./number/index.js";
|
|
13
13
|
export { Origin } from "./origin/index.js";
|
|
14
|
+
export { Panel, type TabChangeEventDetail } from "./panel/index.js";
|
|
14
15
|
export { type Placement, Popover } from "./popover/index.js";
|
|
15
16
|
export { RadioGroup } from "./radio/index.js";
|
|
16
17
|
export { RadioInput } from "./radio/input.js";
|
|
17
18
|
export { RadioOption } from "./radio/option.js";
|
|
18
19
|
export { CONTROL_CHANGE_EVENT, type ControlEventDetail, coerceNumber, dispatchControlEvent, readControlValue, setBooleanAttribute } from "./shared.js";
|
|
19
20
|
export { Slider } from "./slider/index.js";
|
|
20
|
-
export
|
|
21
|
+
export { State, type StateChangeEventDetail } from "./state/index.js";
|
|
21
22
|
export { Toggle } from "./toggle/index.js";
|
|
22
23
|
export { Tooltip } from "./tooltip/index.js";
|
package/build/elements/index.js
CHANGED
|
@@ -11,12 +11,13 @@ export { Monitor } from "./monitor/index.js";
|
|
|
11
11
|
export { MonitorFps } from "./monitor/fps.js";
|
|
12
12
|
export { NumberInput } from "./number/index.js";
|
|
13
13
|
export { Origin } from "./origin/index.js";
|
|
14
|
+
export { Panel } from "./panel/index.js";
|
|
14
15
|
export { Popover } from "./popover/index.js";
|
|
15
16
|
export { RadioGroup } from "./radio/index.js";
|
|
16
17
|
export { RadioInput } from "./radio/input.js";
|
|
17
18
|
export { RadioOption } from "./radio/option.js";
|
|
18
19
|
export { CONTROL_CHANGE_EVENT, coerceNumber, dispatchControlEvent, readControlValue, setBooleanAttribute } from "./shared.js";
|
|
19
20
|
export { Slider } from "./slider/index.js";
|
|
20
|
-
export
|
|
21
|
+
export { State } from "./state/index.js";
|
|
21
22
|
export { Toggle } from "./toggle/index.js";
|
|
22
23
|
export { Tooltip } from "./tooltip/index.js";
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Panel = void 0;
|
|
4
|
+
const lit_html_1 = require("lit-html");
|
|
5
|
+
const shared_1 = require("../shared.cjs");
|
|
6
|
+
const Component_1 = require("../../decorators/Component");
|
|
7
|
+
const Listen_1 = require("../../decorators/Listen");
|
|
8
|
+
const Prop_1 = require("../../decorators/Prop");
|
|
9
|
+
const Query_1 = require("../../decorators/Query");
|
|
10
|
+
/**
|
|
11
|
+
* Panel component - visual container with optional tabs and header actions.
|
|
12
|
+
*
|
|
13
|
+
* Use this component when you want the panel UI without state management,
|
|
14
|
+
* or wrap it around `<ease-state>` for full functionality.
|
|
15
|
+
*
|
|
16
|
+
* @tag ease-panel
|
|
17
|
+
*
|
|
18
|
+
* @slot headline - Panel title text (hidden when tabs are present)
|
|
19
|
+
* @slot actions - Header action buttons, links, or dropdowns
|
|
20
|
+
* @slot - Default slot for main content
|
|
21
|
+
* @slot tab-{id} - Tab panel content (use `data-tab-label` for display name)
|
|
22
|
+
* @slot footer - Footer content below all panels
|
|
23
|
+
*
|
|
24
|
+
* @csspart section - Outer container
|
|
25
|
+
* @csspart header - Header row containing headline/tabs and actions
|
|
26
|
+
* @csspart headline - Title element
|
|
27
|
+
* @csspart tabs - Tab button container
|
|
28
|
+
* @csspart tab - Individual tab button
|
|
29
|
+
* @csspart actions - Actions container
|
|
30
|
+
* @csspart content - Content wrapper (handles height animations)
|
|
31
|
+
* @csspart body - Inner body container
|
|
32
|
+
* @csspart tab-panel - Individual tab panel
|
|
33
|
+
* @csspart footer - Footer container
|
|
34
|
+
*
|
|
35
|
+
* @fires tab-change - Fired when the active tab changes
|
|
36
|
+
*/
|
|
37
|
+
@(0, Component_1.Component)({
|
|
38
|
+
tag: 'ease-panel',
|
|
39
|
+
shadowMode: 'open',
|
|
40
|
+
styles: `
|
|
41
|
+
:host {
|
|
42
|
+
--ease-panel-transition-duration: 120ms;
|
|
43
|
+
--ease-panel-transition-easing: cubic-bezier(.25, 0, .5, 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[part="section"] {
|
|
47
|
+
display: block;
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-width: var(--ease-panel-max-width, 332px);
|
|
50
|
+
border-radius: var(--ease-panel-radius, 12px);
|
|
51
|
+
border: 1px solid var(--ease-panel-border-color, var(--color-white-6));
|
|
52
|
+
background-clip: padding-box;
|
|
53
|
+
background: var(--ease-panel-background, var(--color-gray-1000));
|
|
54
|
+
box-shadow: var(--ease-panel-shadow, 0 0 40px 0 var(--color-white-2) inset);
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
padding: var(--ease-panel-padding, 12px);
|
|
57
|
+
margin: auto;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[part="header"] {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
width: 100%;
|
|
65
|
+
margin-bottom: 12px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[part="header"]:not(:has([part="headline"] slot[name="headline"]::slotted(*))):not(:has([part="tabs"]:not(:empty))):not(:has([part="actions"] slot[name="actions"]::slotted(*))) {
|
|
69
|
+
display: none;
|
|
70
|
+
margin-bottom: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="headline"] {
|
|
74
|
+
font-size: var(--ease-panel-title-font-size, 14px);
|
|
75
|
+
font-weight: var(--ease-panel-title-font-weight, 500);
|
|
76
|
+
line-height: var(--ease-panel-title-line-height, 24px);
|
|
77
|
+
font-family: var(--ease-font-family, "Instrument Sans", sans-serif);
|
|
78
|
+
color: var(--ease-panel-title-color, var(--color-blue-100));
|
|
79
|
+
margin: 0 0 0 4px;
|
|
80
|
+
flex-grow: 1;
|
|
81
|
+
text-ellipsis: ellipsis;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
[part="headline"]:has(+ [part="tabs"]:not(:empty)) {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[part="tabs"] {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 2px;
|
|
94
|
+
flex-grow: 1;
|
|
95
|
+
margin: 0 0 0 4px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[part="tabs"]:empty {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[part="tab"] {
|
|
103
|
+
appearance: none;
|
|
104
|
+
font-size: var(--ease-panel-tab-font-size, 13px);
|
|
105
|
+
font-weight: var(--ease-panel-tab-font-weight, 500);
|
|
106
|
+
line-height: var(--ease-panel-tab-line-height, 24px);
|
|
107
|
+
font-family: var(--ease-font-family, "Instrument Sans", sans-serif);
|
|
108
|
+
color: var(--ease-panel-tab-color, var(--color-gray-600));
|
|
109
|
+
background: transparent;
|
|
110
|
+
border: none;
|
|
111
|
+
padding: 4px 8px;
|
|
112
|
+
margin: 0;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
border-radius: var(--ease-panel-tab-radius, 6px);
|
|
115
|
+
transition: color 0.2s, background-color 0.2s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[part="tab"]:hover {
|
|
119
|
+
color: var(--ease-panel-tab-color-hover, var(--color-blue-100));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[part="tab"][aria-selected="true"] {
|
|
123
|
+
color: var(--ease-panel-tab-color-active, var(--color-blue-100));
|
|
124
|
+
background: var(--ease-panel-tab-background-active, var(--color-white-4));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[part="actions"] {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 4px;
|
|
131
|
+
margin-left: auto;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
slot[name="actions"]::slotted(button),
|
|
135
|
+
slot[name="actions"]::slotted(a) {
|
|
136
|
+
--ease-icon-size: var(--ease-panel-action-icon-size, 16px);
|
|
137
|
+
|
|
138
|
+
appearance: none;
|
|
139
|
+
flex: 0 0 24px;
|
|
140
|
+
border: none;
|
|
141
|
+
outline: none;
|
|
142
|
+
background-color: transparent;
|
|
143
|
+
padding: 4px;
|
|
144
|
+
margin: 0;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
color: var(--color-gray-600);
|
|
147
|
+
transition: color 0.2s;
|
|
148
|
+
text-decoration: none;
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
slot[name="actions"]::slotted(button:hover),
|
|
155
|
+
slot[name="actions"]::slotted(button:focus-visible),
|
|
156
|
+
slot[name="actions"]::slotted(a:hover),
|
|
157
|
+
slot[name="actions"]::slotted(a:focus-visible) {
|
|
158
|
+
color: var(--color-blue-100);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
slot[name="actions"]::slotted(ease-dropdown) {
|
|
162
|
+
flex: 0 0 auto;
|
|
163
|
+
width: auto;
|
|
164
|
+
|
|
165
|
+
--ease-icon-size: var(--ease-panel-action-icon-size, 16px);
|
|
166
|
+
--ease-dropdown-trigger-padding: 4px;
|
|
167
|
+
--ease-dropdown-radius: 6px;
|
|
168
|
+
--ease-dropdown-background: transparent;
|
|
169
|
+
--ease-dropdown-background-hover: transparent;
|
|
170
|
+
--ease-dropdown-shadow: none;
|
|
171
|
+
--ease-dropdown-color: var(--color-gray-600);
|
|
172
|
+
--ease-popover-placement: bottom-end;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
slot[name="actions"]::slotted(ease-dropdown:hover),
|
|
176
|
+
slot[name="actions"]::slotted(ease-dropdown:focus-within) {
|
|
177
|
+
--ease-dropdown-color: var(--color-blue-100);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[part="content"] {
|
|
181
|
+
display: block;
|
|
182
|
+
width: 100%;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
margin: auto;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
[part="content"][data-animating="true"] {
|
|
189
|
+
transition: height var(--ease-panel-transition-duration) var(--ease-panel-transition-easing);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
[part="body"] {
|
|
193
|
+
width: 100%;
|
|
194
|
+
position: relative;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
[part="tab-panel"] {
|
|
198
|
+
width: 100%;
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[part="tab-panel"][data-state="active"] {
|
|
204
|
+
display: block;
|
|
205
|
+
pointer-events: auto;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
[part="tab-panel"][data-state="hidden"] {
|
|
209
|
+
display: none;
|
|
210
|
+
pointer-events: none;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
[part="footer"] {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: space-between;
|
|
217
|
+
width: 100%;
|
|
218
|
+
padding: var(--ease-panel-footer-padding, 12px);
|
|
219
|
+
box-sizing: border-box;
|
|
220
|
+
border-top: 1px solid var(--color-white-4);
|
|
221
|
+
|
|
222
|
+
&:not(:has([data-has-content="true"])) {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
::slotted(:not([slot])),
|
|
228
|
+
::slotted([slot^="tab-"]) {
|
|
229
|
+
display: grid;
|
|
230
|
+
gap: 12px;
|
|
231
|
+
box-sizing: border-box;
|
|
232
|
+
width: 100%;
|
|
233
|
+
}
|
|
234
|
+
`
|
|
235
|
+
})
|
|
236
|
+
class Panel extends HTMLElement {
|
|
237
|
+
#tabs = [];
|
|
238
|
+
#isAnimating = false;
|
|
239
|
+
@(0, Prop_1.Prop)({
|
|
240
|
+
type: Number,
|
|
241
|
+
reflect: true,
|
|
242
|
+
attribute: 'active-tab',
|
|
243
|
+
defaultValue: 0,
|
|
244
|
+
onChange(next, previous) {
|
|
245
|
+
const self = this;
|
|
246
|
+
if (next !== previous && previous !== undefined) {
|
|
247
|
+
self.handleActiveTabChange(previous, next);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
accessor activeTab = 0;
|
|
252
|
+
/** @internal */
|
|
253
|
+
handleActiveTabChange(previous, next) {
|
|
254
|
+
this.performTabAnimation(previous, next);
|
|
255
|
+
}
|
|
256
|
+
@(0, Query_1.Query)('[part="content"]')
|
|
257
|
+
accessor contentElement;
|
|
258
|
+
@(0, Query_1.Query)('[part="body"]')
|
|
259
|
+
accessor bodyElement;
|
|
260
|
+
/**
|
|
261
|
+
* Get the tab configuration
|
|
262
|
+
*/
|
|
263
|
+
get tabs() {
|
|
264
|
+
return this.#tabs;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Switch to a specific tab by index
|
|
268
|
+
* @param index - The tab index (0-based)
|
|
269
|
+
*/
|
|
270
|
+
setTab(index) {
|
|
271
|
+
if (index >= 0 && index < this.#tabs.length && index !== this.activeTab) {
|
|
272
|
+
this.activeTab = index;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
connectedCallback() {
|
|
276
|
+
this.#syncTabs();
|
|
277
|
+
}
|
|
278
|
+
afterRender() {
|
|
279
|
+
this.#syncTabs();
|
|
280
|
+
}
|
|
281
|
+
render() {
|
|
282
|
+
const hasTabs = this.#tabs.length > 0;
|
|
283
|
+
return (0, lit_html_1.html) `
|
|
284
|
+
<section part="section">
|
|
285
|
+
<div part="header">
|
|
286
|
+
<h3 part="headline"><slot name="headline"></slot></h3>
|
|
287
|
+
${this.#renderTabs()}
|
|
288
|
+
<div part="actions">
|
|
289
|
+
<slot name="actions"></slot>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div part="content">
|
|
293
|
+
<div part="body">
|
|
294
|
+
${hasTabs ? this.#renderTabPanels() : (0, lit_html_1.html) `<slot></slot>`}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div part="footer">
|
|
298
|
+
<slot name="footer"></slot>
|
|
299
|
+
</div>
|
|
300
|
+
</section>
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
#renderTabs() {
|
|
304
|
+
if (this.#tabs.length === 0) {
|
|
305
|
+
return lit_html_1.nothing;
|
|
306
|
+
}
|
|
307
|
+
return (0, lit_html_1.html) `
|
|
308
|
+
<div part="tabs" role="tablist">
|
|
309
|
+
${this.#tabs.map((tab, index) => (0, lit_html_1.html) `
|
|
310
|
+
<button
|
|
311
|
+
part="tab"
|
|
312
|
+
role="tab"
|
|
313
|
+
aria-selected=${index === this.activeTab ? 'true' : 'false'}
|
|
314
|
+
aria-controls=${`panel-${tab.id}`}
|
|
315
|
+
tabindex=${index === this.activeTab ? 0 : -1}
|
|
316
|
+
@click=${(e) => this.#handleTabClick(index, tab.id, e)}
|
|
317
|
+
@keydown=${(e) => this.#handleTabKeydown(e, index)}
|
|
318
|
+
>
|
|
319
|
+
${tab.label}
|
|
320
|
+
</button>
|
|
321
|
+
`)}
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
#renderTabPanels() {
|
|
326
|
+
return (0, lit_html_1.html) `
|
|
327
|
+
${this.#tabs.map((tab, index) => (0, lit_html_1.html) `
|
|
328
|
+
<div
|
|
329
|
+
part="tab-panel"
|
|
330
|
+
role="tabpanel"
|
|
331
|
+
id=${`panel-${tab.id}`}
|
|
332
|
+
aria-labelledby=${`tab-${tab.id}`}
|
|
333
|
+
data-state=${index === this.activeTab ? 'active' : 'hidden'}
|
|
334
|
+
data-index=${index}
|
|
335
|
+
>
|
|
336
|
+
<slot name=${`tab-${tab.id}`}></slot>
|
|
337
|
+
</div>
|
|
338
|
+
`)}
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
#handleTabClick(index, id, event) {
|
|
342
|
+
if (index === this.activeTab) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.activeTab = index;
|
|
346
|
+
this.dispatchEvent(new CustomEvent('tab-change', {
|
|
347
|
+
detail: { index, id, event },
|
|
348
|
+
bubbles: true,
|
|
349
|
+
composed: true
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
#handleTabKeydown(event, currentIndex) {
|
|
353
|
+
let newIndex = currentIndex;
|
|
354
|
+
switch (event.key) {
|
|
355
|
+
case 'ArrowLeft':
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : this.#tabs.length - 1;
|
|
358
|
+
break;
|
|
359
|
+
case 'ArrowRight':
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
newIndex = currentIndex < this.#tabs.length - 1 ? currentIndex + 1 : 0;
|
|
362
|
+
break;
|
|
363
|
+
case 'Home':
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
newIndex = 0;
|
|
366
|
+
break;
|
|
367
|
+
case 'End':
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
newIndex = this.#tabs.length - 1;
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (newIndex !== currentIndex) {
|
|
375
|
+
this.activeTab = newIndex;
|
|
376
|
+
// Focus the new tab button
|
|
377
|
+
queueMicrotask(() => {
|
|
378
|
+
const tabButtons = this.shadowRoot?.querySelectorAll('[part="tab"]');
|
|
379
|
+
const newTabButton = tabButtons?.[newIndex];
|
|
380
|
+
newTabButton?.focus();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async performTabAnimation(fromIndex, toIndex) {
|
|
385
|
+
if (this.#isAnimating) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.#isAnimating = true;
|
|
389
|
+
const duration = 120;
|
|
390
|
+
const easing = 'cubic-bezier(.25, 0, .5, 1)';
|
|
391
|
+
const content = this.contentElement;
|
|
392
|
+
if (!content) {
|
|
393
|
+
this.#isAnimating = false;
|
|
394
|
+
this.requestRender();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Get the panels by data-index attribute for reliability
|
|
398
|
+
const fromPanel = this.shadowRoot?.querySelector(`[part="tab-panel"][data-index="${fromIndex}"]`);
|
|
399
|
+
const toPanel = this.shadowRoot?.querySelector(`[part="tab-panel"][data-index="${toIndex}"]`);
|
|
400
|
+
if (!fromPanel || !toPanel) {
|
|
401
|
+
this.#isAnimating = false;
|
|
402
|
+
this.requestRender();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// Lock the current height
|
|
406
|
+
const startHeight = content.getBoundingClientRect().height;
|
|
407
|
+
content.style.height = `${startHeight}px`;
|
|
408
|
+
// FIX: Ensure the new panel is hidden immediately.
|
|
409
|
+
toPanel.style.display = 'none';
|
|
410
|
+
toPanel.style.opacity = '0';
|
|
411
|
+
// Fade out old content via WAAPI
|
|
412
|
+
try {
|
|
413
|
+
const fadeOut = fromPanel.animate([{ opacity: 1 }, { opacity: 0 }], { duration, easing, fill: 'forwards' });
|
|
414
|
+
await fadeOut.finished;
|
|
415
|
+
fadeOut.cancel();
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// ignore
|
|
419
|
+
}
|
|
420
|
+
fromPanel.setAttribute('data-state', 'hidden');
|
|
421
|
+
// Prepare and measure new panel while completely invisible
|
|
422
|
+
const previousToState = toPanel.getAttribute('data-state');
|
|
423
|
+
toPanel.style.display = 'block';
|
|
424
|
+
toPanel.style.visibility = 'hidden';
|
|
425
|
+
toPanel.style.opacity = '0';
|
|
426
|
+
// Force layout, then measure
|
|
427
|
+
void toPanel.offsetHeight;
|
|
428
|
+
const endHeight = toPanel.getBoundingClientRect().height;
|
|
429
|
+
// Animate height
|
|
430
|
+
if (startHeight !== endHeight) {
|
|
431
|
+
content.setAttribute('data-animating', 'true');
|
|
432
|
+
void content.offsetHeight;
|
|
433
|
+
content.style.height = `${endHeight}px`;
|
|
434
|
+
await this.#wait(duration);
|
|
435
|
+
}
|
|
436
|
+
// Show panel but keep opacity at 0, then fade in
|
|
437
|
+
toPanel.style.visibility = 'visible';
|
|
438
|
+
toPanel.style.opacity = '0';
|
|
439
|
+
void toPanel.offsetHeight;
|
|
440
|
+
try {
|
|
441
|
+
const fadeIn = toPanel.animate([{ opacity: 0 }, { opacity: 1 }], { duration, easing, fill: 'forwards' });
|
|
442
|
+
await fadeIn.finished;
|
|
443
|
+
fadeIn.cancel();
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
// Finalize new tab state and cleanup
|
|
449
|
+
toPanel.style.display = '';
|
|
450
|
+
toPanel.style.visibility = '';
|
|
451
|
+
toPanel.style.opacity = '';
|
|
452
|
+
if (previousToState !== 'active') {
|
|
453
|
+
toPanel.setAttribute('data-state', 'active');
|
|
454
|
+
}
|
|
455
|
+
content.style.height = '';
|
|
456
|
+
content.removeAttribute('data-animating');
|
|
457
|
+
this.#isAnimating = false;
|
|
458
|
+
}
|
|
459
|
+
#wait(ms) {
|
|
460
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
461
|
+
}
|
|
462
|
+
#syncTabs() {
|
|
463
|
+
const tabs = [];
|
|
464
|
+
for (const child of Array.from(this.children)) {
|
|
465
|
+
const slot = child.getAttribute('slot');
|
|
466
|
+
if (slot?.startsWith('tab-')) {
|
|
467
|
+
const id = slot.replace('tab-', '');
|
|
468
|
+
const label = child.getAttribute('data-tab-label') || id;
|
|
469
|
+
tabs.push({ id, label });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.#tabs = tabs.slice(0, 3);
|
|
473
|
+
if (this.activeTab >= this.#tabs.length && this.#tabs.length > 0) {
|
|
474
|
+
this.activeTab = 0;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
@(0, Listen_1.Listen)('slotchange', { selector: 'slot[name="footer"]' })
|
|
478
|
+
onFooterSlotChange() {
|
|
479
|
+
this.updateFooterAttribute();
|
|
480
|
+
}
|
|
481
|
+
@(0, Listen_1.Listen)('slotchange', { selector: 'slot:not([name])' })
|
|
482
|
+
onDefaultSlotChange() {
|
|
483
|
+
this.#syncTabs();
|
|
484
|
+
this.requestRender();
|
|
485
|
+
}
|
|
486
|
+
updateFooterAttribute() {
|
|
487
|
+
const footer = this.shadowRoot?.querySelector('[part="footer"]');
|
|
488
|
+
if (!footer) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const footerSlot = this.shadowRoot?.querySelector('slot[name="footer"]');
|
|
492
|
+
const hasFooter = Boolean(footerSlot?.assignedNodes({ flatten: true }).length > 0);
|
|
493
|
+
(0, shared_1.setBooleanAttribute)(footer, 'data-has-content', hasFooter);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
exports.Panel = Panel;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit-html';
|
|
2
|
+
/**
|
|
3
|
+
* Event detail for tab change events
|
|
4
|
+
*/
|
|
5
|
+
export interface TabChangeEventDetail {
|
|
6
|
+
/** The index of the active tab */
|
|
7
|
+
index: number;
|
|
8
|
+
/** The tab id */
|
|
9
|
+
id: string;
|
|
10
|
+
/** The original event */
|
|
11
|
+
event: Event;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Panel component - visual container with optional tabs and header actions.
|
|
15
|
+
*
|
|
16
|
+
* Use this component when you want the panel UI without state management,
|
|
17
|
+
* or wrap it around `<ease-state>` for full functionality.
|
|
18
|
+
*
|
|
19
|
+
* @tag ease-panel
|
|
20
|
+
*
|
|
21
|
+
* @slot headline - Panel title text (hidden when tabs are present)
|
|
22
|
+
* @slot actions - Header action buttons, links, or dropdowns
|
|
23
|
+
* @slot - Default slot for main content
|
|
24
|
+
* @slot tab-{id} - Tab panel content (use `data-tab-label` for display name)
|
|
25
|
+
* @slot footer - Footer content below all panels
|
|
26
|
+
*
|
|
27
|
+
* @csspart section - Outer container
|
|
28
|
+
* @csspart header - Header row containing headline/tabs and actions
|
|
29
|
+
* @csspart headline - Title element
|
|
30
|
+
* @csspart tabs - Tab button container
|
|
31
|
+
* @csspart tab - Individual tab button
|
|
32
|
+
* @csspart actions - Actions container
|
|
33
|
+
* @csspart content - Content wrapper (handles height animations)
|
|
34
|
+
* @csspart body - Inner body container
|
|
35
|
+
* @csspart tab-panel - Individual tab panel
|
|
36
|
+
* @csspart footer - Footer container
|
|
37
|
+
*
|
|
38
|
+
* @fires tab-change - Fired when the active tab changes
|
|
39
|
+
*/
|
|
40
|
+
export declare class Panel extends HTMLElement {
|
|
41
|
+
#private;
|
|
42
|
+
requestRender: () => void;
|
|
43
|
+
accessor activeTab: number;
|
|
44
|
+
/** @internal */
|
|
45
|
+
handleActiveTabChange(previous: number, next: number): void;
|
|
46
|
+
accessor contentElement: HTMLElement | null;
|
|
47
|
+
accessor bodyElement: HTMLElement | null;
|
|
48
|
+
/**
|
|
49
|
+
* Get the tab configuration
|
|
50
|
+
*/
|
|
51
|
+
get tabs(): ReadonlyArray<{
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Switch to a specific tab by index
|
|
57
|
+
* @param index - The tab index (0-based)
|
|
58
|
+
*/
|
|
59
|
+
setTab(index: number): void;
|
|
60
|
+
connectedCallback(): void;
|
|
61
|
+
afterRender(): void;
|
|
62
|
+
render(): TemplateResult;
|
|
63
|
+
performTabAnimation(fromIndex: number, toIndex: number): Promise<void>;
|
|
64
|
+
onFooterSlotChange(): void;
|
|
65
|
+
onDefaultSlotChange(): void;
|
|
66
|
+
private updateFooterAttribute;
|
|
67
|
+
}
|