@easemate/web-kit 0.1.4 → 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.
Files changed (162) hide show
  1. package/README.md +360 -168
  2. package/build/components/code/index.cjs +3 -3
  3. package/build/components/code/index.js +3 -3
  4. package/build/components/curve/canvas-controls.cjs +3 -3
  5. package/build/components/curve/canvas-controls.js +3 -3
  6. package/build/components/curve/canvas.cjs +4 -4
  7. package/build/components/curve/canvas.js +4 -4
  8. package/build/components/curve/controls.cjs +6 -6
  9. package/build/components/curve/controls.d.cts +4 -4
  10. package/build/components/curve/controls.d.ts +4 -4
  11. package/build/components/curve/controls.js +6 -6
  12. package/build/components/curve/index.cjs +3 -3
  13. package/build/components/curve/index.d.cts +1 -1
  14. package/build/components/curve/index.d.ts +1 -1
  15. package/build/components/curve/index.js +3 -3
  16. package/build/components/curve/output.cjs +3 -3
  17. package/build/components/curve/output.d.cts +1 -1
  18. package/build/components/curve/output.d.ts +1 -1
  19. package/build/components/curve/output.js +3 -3
  20. package/build/components/curve/toolbar.cjs +7 -7
  21. package/build/components/curve/toolbar.d.cts +4 -4
  22. package/build/components/curve/toolbar.d.ts +4 -4
  23. package/build/components/curve/toolbar.js +7 -7
  24. package/build/decorators/OutsideClick.cjs +2 -2
  25. package/build/decorators/OutsideClick.d.cts +2 -2
  26. package/build/decorators/OutsideClick.d.ts +2 -2
  27. package/build/decorators/OutsideClick.js +2 -2
  28. package/build/elements/button/index.cjs +2 -2
  29. package/build/elements/button/index.js +2 -2
  30. package/build/elements/checkbox/index.cjs +4 -4
  31. package/build/elements/checkbox/index.js +4 -4
  32. package/build/elements/color/index.cjs +4 -4
  33. package/build/elements/color/index.d.cts +1 -1
  34. package/build/elements/color/index.d.ts +1 -1
  35. package/build/elements/color/index.js +4 -4
  36. package/build/elements/color/picker.cjs +4 -4
  37. package/build/elements/color/picker.js +4 -4
  38. package/build/elements/dropdown/index.cjs +4 -4
  39. package/build/elements/dropdown/index.d.cts +1 -1
  40. package/build/elements/dropdown/index.d.ts +1 -1
  41. package/build/elements/dropdown/index.js +4 -4
  42. package/build/elements/field/index.cjs +2 -2
  43. package/build/elements/field/index.js +2 -2
  44. package/build/elements/icons/animation/chevron.cjs +2 -2
  45. package/build/elements/icons/animation/chevron.js +2 -2
  46. package/build/elements/icons/animation/clear.cjs +1 -1
  47. package/build/elements/icons/animation/clear.js +1 -1
  48. package/build/elements/icons/animation/grid.cjs +2 -2
  49. package/build/elements/icons/animation/grid.js +2 -2
  50. package/build/elements/icons/animation/loading.cjs +1 -1
  51. package/build/elements/icons/animation/loading.js +1 -1
  52. package/build/elements/icons/animation/snap.cjs +2 -2
  53. package/build/elements/icons/animation/snap.js +2 -2
  54. package/build/elements/icons/interface/anchor-add.cjs +1 -1
  55. package/build/elements/icons/interface/anchor-add.js +1 -1
  56. package/build/elements/icons/interface/anchor-remove.cjs +1 -1
  57. package/build/elements/icons/interface/anchor-remove.js +1 -1
  58. package/build/elements/icons/interface/arrow-up.cjs +1 -1
  59. package/build/elements/icons/interface/arrow-up.js +1 -1
  60. package/build/elements/icons/interface/arrows-vertical.cjs +1 -1
  61. package/build/elements/icons/interface/arrows-vertical.js +1 -1
  62. package/build/elements/icons/interface/bezier-angle.cjs +1 -1
  63. package/build/elements/icons/interface/bezier-angle.js +1 -1
  64. package/build/elements/icons/interface/bezier-distribute.cjs +1 -1
  65. package/build/elements/icons/interface/bezier-distribute.js +1 -1
  66. package/build/elements/icons/interface/bezier-length.cjs +1 -1
  67. package/build/elements/icons/interface/bezier-length.js +1 -1
  68. package/build/elements/icons/interface/bezier-mirror.cjs +1 -1
  69. package/build/elements/icons/interface/bezier-mirror.js +1 -1
  70. package/build/elements/icons/interface/bezier.cjs +1 -1
  71. package/build/elements/icons/interface/bezier.js +1 -1
  72. package/build/elements/icons/interface/check.cjs +1 -1
  73. package/build/elements/icons/interface/check.js +1 -1
  74. package/build/elements/icons/interface/circle-arrow-left.cjs +1 -1
  75. package/build/elements/icons/interface/circle-arrow-left.js +1 -1
  76. package/build/elements/icons/interface/circle-arrow-right.cjs +1 -1
  77. package/build/elements/icons/interface/circle-arrow-right.js +1 -1
  78. package/build/elements/icons/interface/code.cjs +1 -1
  79. package/build/elements/icons/interface/code.js +1 -1
  80. package/build/elements/icons/interface/dots.cjs +1 -1
  81. package/build/elements/icons/interface/dots.js +1 -1
  82. package/build/elements/icons/interface/mention.cjs +1 -1
  83. package/build/elements/icons/interface/mention.js +1 -1
  84. package/build/elements/icons/interface/minus.cjs +1 -1
  85. package/build/elements/icons/interface/minus.js +1 -1
  86. package/build/elements/icons/interface/picker.cjs +1 -1
  87. package/build/elements/icons/interface/picker.js +1 -1
  88. package/build/elements/icons/interface/plus.cjs +1 -1
  89. package/build/elements/icons/interface/plus.js +1 -1
  90. package/build/elements/icons/interface/settings.cjs +1 -1
  91. package/build/elements/icons/interface/settings.js +1 -1
  92. package/build/elements/index.cjs +5 -2
  93. package/build/elements/index.d.cts +2 -1
  94. package/build/elements/index.d.ts +2 -1
  95. package/build/elements/index.js +2 -1
  96. package/build/elements/input/index.cjs +4 -4
  97. package/build/elements/input/index.js +4 -4
  98. package/build/elements/logo/index.cjs +2 -2
  99. package/build/elements/logo/index.js +2 -2
  100. package/build/elements/monitor/fps.cjs +3 -3
  101. package/build/elements/monitor/fps.js +3 -3
  102. package/build/elements/monitor/index.cjs +4 -4
  103. package/build/elements/monitor/index.js +4 -4
  104. package/build/elements/number/index.cjs +4 -4
  105. package/build/elements/number/index.js +4 -4
  106. package/build/elements/origin/index.cjs +4 -4
  107. package/build/elements/origin/index.js +4 -4
  108. package/build/elements/panel/index.cjs +496 -0
  109. package/build/elements/panel/index.d.cts +67 -0
  110. package/build/elements/panel/index.d.ts +67 -0
  111. package/build/elements/panel/index.js +492 -0
  112. package/build/elements/popover/index.cjs +2 -2
  113. package/build/elements/popover/index.js +2 -2
  114. package/build/elements/radio/index.cjs +3 -3
  115. package/build/elements/radio/index.js +3 -3
  116. package/build/elements/radio/input.cjs +4 -4
  117. package/build/elements/radio/input.js +4 -4
  118. package/build/elements/slider/index.cjs +4 -4
  119. package/build/elements/slider/index.js +4 -4
  120. package/build/elements/state/index.cjs +61 -468
  121. package/build/elements/state/index.d.cts +34 -25
  122. package/build/elements/state/index.d.ts +34 -25
  123. package/build/elements/state/index.js +63 -470
  124. package/build/elements/toggle/index.cjs +4 -4
  125. package/build/elements/toggle/index.js +4 -4
  126. package/build/elements/tooltip/index.cjs +4 -4
  127. package/build/elements/tooltip/index.d.cts +1 -1
  128. package/build/elements/tooltip/index.d.ts +1 -1
  129. package/build/elements/tooltip/index.js +4 -4
  130. package/build/internal/component-loaders.cjs +2 -0
  131. package/build/internal/component-loaders.d.cts +2 -2
  132. package/build/internal/component-loaders.d.ts +2 -2
  133. package/build/internal/component-loaders.js +2 -0
  134. package/build/react/events.cjs +25 -0
  135. package/build/react/events.d.cts +39 -0
  136. package/build/react/events.d.ts +39 -0
  137. package/build/react/events.js +22 -0
  138. package/build/react/index.cjs +19 -0
  139. package/build/react/index.d.cts +13 -0
  140. package/build/react/index.d.ts +13 -0
  141. package/build/react/index.js +12 -0
  142. package/build/react/provider.cjs +134 -0
  143. package/build/react/provider.d.cts +81 -0
  144. package/build/react/provider.d.ts +81 -0
  145. package/build/react/provider.js +98 -0
  146. package/build/react/types.cjs +8 -0
  147. package/build/react/types.d.cts +55 -0
  148. package/build/react/types.d.ts +55 -0
  149. package/build/react/types.js +7 -0
  150. package/build/react/use-ease-state.cjs +129 -0
  151. package/build/react/use-ease-state.d.cts +95 -0
  152. package/build/react/use-ease-state.d.ts +95 -0
  153. package/build/react/use-ease-state.js +126 -0
  154. package/build/react/use-web-kit.cjs +150 -0
  155. package/build/react/use-web-kit.d.cts +80 -0
  156. package/build/react/use-web-kit.d.ts +80 -0
  157. package/build/react/use-web-kit.js +114 -0
  158. package/build/register.cjs +1 -0
  159. package/build/register.d.cts +1 -0
  160. package/build/register.d.ts +1 -0
  161. package/build/register.js +1 -0
  162. package/package.json +16 -2
@@ -18,27 +18,45 @@ export interface StateChangeEventDetail {
18
18
  event: Event;
19
19
  }
20
20
  /**
21
- * Event detail for tab change events
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 activeTab: number;
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,9 +1,9 @@
1
- import { html, nothing } from 'lit-html';
2
- import { CONTROL_CHANGE_EVENT, dispatchControlEvent, setBooleanAttribute } from "../shared.js";
3
- import { Component } from '~/decorators/Component';
4
- import { Listen } from '~/decorators/Listen';
5
- import { Prop } from '~/decorators/Prop';
6
- import { Query } from '~/decorators/Query';
1
+ import { html } from 'lit-html';
2
+ import { CONTROL_CHANGE_EVENT, dispatchControlEvent } from "../shared.js";
3
+ import { Component } from '../../decorators/Component';
4
+ import { Listen } from '../../decorators/Listen';
5
+ import { Prop } from '../../decorators/Prop';
6
+ import { Query } from '../../decorators/Query';
7
7
  const readControlValue = (element) => {
8
8
  if (typeof element.value === 'string' || typeof element.value === 'number') {
9
9
  return element.value;
@@ -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
- --ease-state-transition-duration: 120ms;
34
- --ease-state-transition-easing: cubic-bezier(.25, 0, .5, 1);
68
+ display: contents;
35
69
  }
36
70
 
37
- [part="section"] {
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
- @Prop({
232
- type: Number,
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.entrySlot?.addEventListener('slotchange', this.#handleSlotChange);
154
+ this.defaultSlot?.addEventListener('slotchange', this.#handleSlotChange);
333
155
  }
334
156
  disconnectedCallback() {
335
157
  this.#detach();
336
- this.entrySlot?.removeEventListener('slotchange', this.#handleSlotChange);
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
- <section part="section">
348
- <div part="header">
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 slots = [];
586
- if (this.#tabs.length === 0) {
587
- if (this.entrySlot) {
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
- for (const slot of slots) {
625
- const elements = slot.assignedElements({ flatten: true });
626
- for (const element of elements) {
627
- const controls = findControls(element);
628
- for (const control of controls) {
629
- const name = getControlName(control);
630
- if (name) {
631
- this.#controls.set(name, control);
632
- const value = readControlValue(control);
633
- this.#state[name] = value;
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
  }