kempo-ui 0.0.39 → 0.0.41

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 (67) hide show
  1. package/dist/components/Dropdown.js +123 -0
  2. package/dist/components/ShadowComponent.js +1 -1
  3. package/dist/components/Spinner.js +113 -0
  4. package/docs/components/accordion.html +1 -1
  5. package/docs/components/card.html +1 -1
  6. package/docs/components/color-picker.html +1 -1
  7. package/docs/components/content-slider.html +1 -1
  8. package/docs/components/dialog.html +1 -1
  9. package/docs/components/dropdown.html +282 -0
  10. package/docs/components/focus-capture.html +1 -1
  11. package/docs/components/hybrid-component.html +1 -1
  12. package/docs/components/icon.html +1 -1
  13. package/docs/components/import.html +1 -1
  14. package/docs/components/light-component.html +1 -1
  15. package/docs/components/photo-viewer.html +1 -1
  16. package/docs/components/resize.html +1 -1
  17. package/docs/components/shadow-component.html +1 -1
  18. package/docs/components/show-more.html +1 -1
  19. package/docs/components/side-menu.html +1 -1
  20. package/docs/components/sortable.html +1 -1
  21. package/docs/components/spinner.html +214 -0
  22. package/docs/components/split.html +1 -1
  23. package/docs/components/table.html +1 -1
  24. package/docs/components/tableControls.html +1 -1
  25. package/docs/components/tableCustomFields.html +1 -1
  26. package/docs/components/tableFetchRecords.html +1 -1
  27. package/docs/components/tableFieldSortHide.html +1 -1
  28. package/docs/components/tablePagination.html +1 -1
  29. package/docs/components/tableRecordEditing.html +1 -1
  30. package/docs/components/tableRecordFiltering.html +1 -1
  31. package/docs/components/tableRecordHiding.html +1 -1
  32. package/docs/components/tableRecordSearching.html +1 -1
  33. package/docs/components/tableRecordSelection.html +1 -1
  34. package/docs/components/tableRowControls.html +1 -1
  35. package/docs/components/tableSorting.html +1 -1
  36. package/docs/components/tabs.html +1 -1
  37. package/docs/components/tags.html +1 -1
  38. package/docs/components/theme-switcher.html +1 -1
  39. package/docs/components/timestamp.html +1 -1
  40. package/docs/components/toast.html +1 -1
  41. package/docs/components/toggle.html +1 -1
  42. package/docs/components/tree.html +1 -1
  43. package/docs/index.html +13 -1
  44. package/docs/nav-1.inc.html +2 -0
  45. package/docs/src/components/Dropdown.js +123 -0
  46. package/docs/src/components/ShadowComponent.js +1 -1
  47. package/docs/src/components/Spinner.js +113 -0
  48. package/docs/utils/context.html +1 -1
  49. package/docs/utils/cookie.html +1 -1
  50. package/docs/utils/debounce.html +1 -1
  51. package/docs/utils/drag.html +1 -1
  52. package/docs/utils/formatTimestamp.html +1 -1
  53. package/docs/utils/object.html +1 -1
  54. package/docs/utils/propConverters.html +1 -1
  55. package/docs/utils/string.html +1 -1
  56. package/docs/utils/theme.html +1 -1
  57. package/docs/utils/toTitleCase.html +1 -1
  58. package/docs/utils/type.html +1 -1
  59. package/docs/utils/wait.html +1 -1
  60. package/package.json +1 -1
  61. package/src/components/Dropdown.js +358 -0
  62. package/src/components/ShadowComponent.js +1 -1
  63. package/src/components/Spinner.js +152 -0
  64. package/tests/components/Dropdown.browser-test.js +557 -0
  65. package/tests/components/HybridComponent.browser-test.js +8 -2
  66. package/tests/components/ShadowComponent.browser-test.js +6 -6
  67. package/tests/components/Spinner.browser-test.js +335 -0
@@ -0,0 +1,358 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import { boolTrueFalse } from '../utils/propConverters.js';
4
+
5
+ // Track all open dropdowns for mutual exclusion
6
+ const openDropdowns = new Set();
7
+
8
+ export default class Dropdown extends ShadowComponent {
9
+ static properties = {
10
+ opened: { type: Boolean, reflect: true },
11
+ openDirection: { type: String, reflect: true, attribute: 'open-direction' },
12
+ closeOnSelect: { type: Boolean, reflect: true, attribute: 'close-on-select', converter: boolTrueFalse },
13
+ closeOnClickOutside: { type: Boolean, reflect: true, attribute: 'close-on-click-outside', converter: boolTrueFalse }
14
+ };
15
+
16
+ anchorId = `dropdown-anchor-${Math.random().toString(36).slice(2, 11)}`;
17
+
18
+ constructor() {
19
+ super();
20
+ this.opened = false;
21
+ this.openDirection = 'down left';
22
+ this.closeOnSelect = true;
23
+ this.closeOnClickOutside = true;
24
+ }
25
+
26
+ /*
27
+ Lifecycle Callbacks
28
+ */
29
+ connectedCallback() {
30
+ super.connectedCallback();
31
+ document.addEventListener('click', this.handleDocumentClick);
32
+ document.addEventListener('keydown', this.handleKeydown);
33
+ }
34
+
35
+ disconnectedCallback() {
36
+ super.disconnectedCallback();
37
+ document.removeEventListener('click', this.handleDocumentClick);
38
+ document.removeEventListener('keydown', this.handleKeydown);
39
+ openDropdowns.delete(this);
40
+ }
41
+
42
+ updated(changedProperties) {
43
+ super.updated(changedProperties);
44
+ if(changedProperties.has('opened')) {
45
+ if(this.opened) {
46
+ openDropdowns.add(this);
47
+ } else {
48
+ openDropdowns.delete(this);
49
+ }
50
+ this.dispatchEvent(new CustomEvent(this.opened ? 'opened' : 'closed', {
51
+ bubbles: true
52
+ }));
53
+ }
54
+ }
55
+
56
+ /*
57
+ Event Handlers
58
+ */
59
+ handleDocumentClick = e => {
60
+ const clickedDropdown = e.target.closest('k-dropdown');
61
+ const isTrigger = e.target.closest('[slot="trigger"]');
62
+
63
+ // If clicking another dropdown's trigger, close this one
64
+ if(clickedDropdown && clickedDropdown !== this && isTrigger) {
65
+ if(this.opened) this.close();
66
+ return;
67
+ }
68
+
69
+ if(!this.opened) return;
70
+
71
+ // Click is outside this dropdown entirely
72
+ if(clickedDropdown !== this) {
73
+ if(this.closeOnClickOutside) this.close();
74
+ return;
75
+ }
76
+
77
+ // Click is on this dropdown's trigger - let handleTriggerClick handle it
78
+ if(isTrigger) return;
79
+
80
+ // Click is inside the menu on an interactive element
81
+ const isInteractive = e.target.closest('a, button');
82
+ if(isInteractive && this.closeOnSelect) this.close();
83
+ };
84
+
85
+ handleTriggerClick = e => {
86
+ e.stopPropagation();
87
+ // Close all other dropdowns before opening this one
88
+ if(!this.opened) {
89
+ openDropdowns.forEach(dropdown => {
90
+ if(dropdown !== this) dropdown.close();
91
+ });
92
+ }
93
+ this.toggle();
94
+ };
95
+
96
+ handleMenuClick = e => {
97
+ const item = e.target.closest('a, button');
98
+ if(item && !item.hasAttribute('disabled')) {
99
+ const value = item.dataset?.value || item.textContent.trim();
100
+ this.dispatchEvent(new CustomEvent('select', {
101
+ detail: { value, item },
102
+ bubbles: true
103
+ }));
104
+ }
105
+ };
106
+
107
+ handleKeydown = e => {
108
+ if(!this.opened) return;
109
+ if(e.key === 'Escape') {
110
+ e.preventDefault();
111
+ this.close();
112
+ this.focusTrigger();
113
+ } else if(e.key === 'ArrowDown') {
114
+ e.preventDefault();
115
+ this.focusNextItem();
116
+ } else if(e.key === 'ArrowUp') {
117
+ e.preventDefault();
118
+ this.focusPreviousItem();
119
+ } else if(e.key === 'Enter' || e.key === ' ') {
120
+ const focused = this.querySelector('a:focus, button:focus');
121
+ if(focused) {
122
+ e.preventDefault();
123
+ focused.click();
124
+ }
125
+ }
126
+ };
127
+
128
+ /*
129
+ Public Methods
130
+ */
131
+ open() {
132
+ // Close all other dropdowns first
133
+ openDropdowns.forEach(dropdown => {
134
+ if(dropdown !== this) dropdown.close();
135
+ });
136
+ this.opened = true;
137
+ requestAnimationFrame(() => this.focusFirstItem());
138
+ return this;
139
+ }
140
+
141
+ close() {
142
+ this.opened = false;
143
+ return this;
144
+ }
145
+
146
+ toggle() {
147
+ return this.opened ? this.close() : this.open();
148
+ }
149
+
150
+ /*
151
+ Private Methods
152
+ */
153
+ focusTrigger() {
154
+ const trigger = this.querySelector('[slot="trigger"]');
155
+ if(trigger) trigger.focus();
156
+ }
157
+
158
+ getMenuItems() {
159
+ return [...this.querySelectorAll('a, button')].filter(
160
+ el => !el.hasAttribute('disabled') && !el.closest('[slot="trigger"]')
161
+ );
162
+ }
163
+
164
+ focusFirstItem() {
165
+ const items = this.getMenuItems();
166
+ if(items.length > 0) items[0].focus();
167
+ }
168
+
169
+ focusNextItem() {
170
+ const items = this.getMenuItems();
171
+ const current = document.activeElement;
172
+ const index = items.indexOf(current);
173
+ const next = items[index + 1] || items[0];
174
+ if(next) next.focus();
175
+ }
176
+
177
+ focusPreviousItem() {
178
+ const items = this.getMenuItems();
179
+ const current = document.activeElement;
180
+ const index = items.indexOf(current);
181
+ const prev = items[index - 1] || items[items.length - 1];
182
+ if(prev) prev.focus();
183
+ }
184
+
185
+ getPositionArea() {
186
+ const dir = this.openDirection.toLowerCase().trim();
187
+ const parts = dir.split(/\s+/);
188
+ // Map friendly names to position-area values
189
+ const mapping = {
190
+ 'down': 'bottom',
191
+ 'up': 'top',
192
+ 'left': 'left',
193
+ 'right': 'right',
194
+ 'center': 'center'
195
+ };
196
+ const mapped = parts.map(p => mapping[p] || p);
197
+ // position-area expects: row column (e.g., "bottom left")
198
+ // "down left" -> "bottom left", "up right" -> "top right"
199
+ return mapped.join(' ');
200
+ }
201
+
202
+ getFallbacks() {
203
+ const primary = this.getPositionArea();
204
+ const parts = primary.split(' ');
205
+ // Generate sensible fallbacks
206
+ const fallbacks = [];
207
+ // flip-block flips vertical, flip-inline flips horizontal
208
+ fallbacks.push('flip-block');
209
+ fallbacks.push('flip-inline');
210
+ fallbacks.push('flip-block flip-inline');
211
+ return fallbacks.join(', ');
212
+ }
213
+
214
+ /*
215
+ Styles
216
+ */
217
+ static styles = css`
218
+ :host {
219
+ display: inline-block;
220
+ position: relative;
221
+ }
222
+ #trigger {
223
+ cursor: pointer;
224
+ anchor-name: --dropdown-trigger;
225
+ }
226
+ #menu {
227
+ display: none;
228
+ position: fixed;
229
+ position-anchor: --dropdown-trigger;
230
+ z-index: 1000;
231
+ min-width: anchor-size(width);
232
+ background: var(--c_bg);
233
+ border: 1px solid var(--c_border);
234
+ box-shadow: var(--drop_shadow);
235
+ margin: 0.25rem;
236
+ }
237
+ :host([opened]) #menu {
238
+ display: block;
239
+ }
240
+ /* Default: down left */
241
+ #menu {
242
+ position-area: bottom span-right;
243
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
244
+ }
245
+ /* down right */
246
+ :host([open-direction="down right"]) #menu {
247
+ position-area: bottom span-left;
248
+ }
249
+ /* down center */
250
+ :host([open-direction="down center"]) #menu,
251
+ :host([open-direction="down"]) #menu {
252
+ position-area: bottom center;
253
+ }
254
+ /* up left */
255
+ :host([open-direction="up left"]) #menu {
256
+ position-area: top span-right;
257
+ }
258
+ /* up right */
259
+ :host([open-direction="up right"]) #menu {
260
+ position-area: top span-left;
261
+ }
262
+ /* up center */
263
+ :host([open-direction="up center"]) #menu,
264
+ :host([open-direction="up"]) #menu {
265
+ position-area: top center;
266
+ }
267
+ /* left up */
268
+ :host([open-direction="left up"]) #menu {
269
+ position-area: left span-top;
270
+ }
271
+ /* left down */
272
+ :host([open-direction="left down"]) #menu {
273
+ position-area: left span-bottom;
274
+ }
275
+ /* left center */
276
+ :host([open-direction="left center"]) #menu,
277
+ :host([open-direction="left"]) #menu {
278
+ position-area: left center;
279
+ }
280
+ /* right up */
281
+ :host([open-direction="right up"]) #menu {
282
+ position-area: right span-top;
283
+ }
284
+ /* right down */
285
+ :host([open-direction="right down"]) #menu {
286
+ position-area: right span-bottom;
287
+ }
288
+ /* right center */
289
+ :host([open-direction="right center"]) #menu,
290
+ :host([open-direction="right"]) #menu {
291
+ position-area: right center;
292
+ }
293
+ /* Slotted menu item styles (not trigger) */
294
+ ::slotted(a:not([slot="trigger"])),
295
+ ::slotted(button:not([slot="trigger"])) {
296
+ all: unset !important;
297
+ display: block !important;
298
+ box-sizing: border-box !important;
299
+ width: 100% !important;
300
+ padding: var(--spacer_h) var(--spacer) !important;
301
+ color: var(--tc) !important;
302
+ background: transparent !important;
303
+ border: none !important;
304
+ border-top: 1px solid var(--c_border) !important;
305
+ border-radius: 0 !important;
306
+ font: inherit !important;
307
+ text-align: left !important;
308
+ cursor: pointer !important;
309
+ white-space: nowrap !important;
310
+ transition: background var(--animation_ms) !important;
311
+ }
312
+ ::slotted(a.k-dropdown-first),
313
+ ::slotted(button.k-dropdown-first) {
314
+ border-top: none !important;
315
+ }
316
+ ::slotted(a:not([slot="trigger"]):hover),
317
+ ::slotted(a:not([slot="trigger"]):focus-visible),
318
+ ::slotted(button:not([slot="trigger"]):hover),
319
+ ::slotted(button:not([slot="trigger"]):focus-visible) {
320
+ background: var(--c_bg__alt) !important;
321
+ outline: none !important;
322
+ }
323
+ ::slotted(a:not([slot="trigger"])[disabled]),
324
+ ::slotted(button:not([slot="trigger"])[disabled]) {
325
+ opacity: 0.5 !important;
326
+ cursor: not-allowed !important;
327
+ pointer-events: none !important;
328
+ }
329
+ ::slotted(hr) {
330
+ display: none !important;
331
+ }
332
+ `;
333
+
334
+ /*
335
+ Slot Change Handler
336
+ */
337
+ handleSlotChange = e => {
338
+ const slot = e.target;
339
+ const items = slot.assignedElements().filter(el => el.matches('a, button'));
340
+ items.forEach((item, i) => item.classList.toggle('k-dropdown-first', i === 0));
341
+ };
342
+
343
+ /*
344
+ Rendering
345
+ */
346
+ render() {
347
+ return html`
348
+ <div id="trigger" @click=${this.handleTriggerClick}>
349
+ <slot name="trigger"></slot>
350
+ </div>
351
+ <div id="menu" role="menu" @click=${this.handleMenuClick}>
352
+ <slot @slotchange=${this.handleSlotChange}></slot>
353
+ </div>
354
+ `;
355
+ }
356
+ }
357
+
358
+ customElements.define('k-dropdown', Dropdown);
@@ -30,7 +30,7 @@ export default class ShadowComponent extends LitElement {
30
30
 
31
31
  const link = document.createElement('link');
32
32
  link.rel = 'stylesheet';
33
- link.href = window.kempo?.pathToStylesheet || 'https://cdn.jsdelivr.net/npm/kempo-css@1.3.1/dist/kempo.min.css';
33
+ link.href = window.kempo?.pathToStylesheet || 'https://cdn.jsdelivr.net/npm/kempo-css@1.3.2/dist/kempo.min.css';
34
34
  shadowRoot.appendChild(link);
35
35
 
36
36
  // Inject component styles if they exist
@@ -0,0 +1,152 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+
4
+ export default class Spinner extends ShadowComponent {
5
+ static properties = {
6
+ size: { type: String, reflect: true },
7
+ variant: { type: String, reflect: true }
8
+ };
9
+
10
+ constructor() {
11
+ super();
12
+ this.size = 'md';
13
+ this.variant = 'spinner';
14
+ }
15
+
16
+ /*
17
+ Styles
18
+ */
19
+ static styles = css`
20
+ :host {
21
+ --spinner-size: 2rem;
22
+ --spinner-border-width: 3px;
23
+ --spinner-color: var(--c_primary);
24
+ --spinner-track-color: var(--c_border);
25
+ display: inline-flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ }
29
+ /* Sizes */
30
+ :host([size="xs"]) {
31
+ --spinner-size: 1rem;
32
+ --spinner-border-width: 2px;
33
+ }
34
+ :host([size="sm"]) {
35
+ --spinner-size: 1.5rem;
36
+ --spinner-border-width: 2px;
37
+ }
38
+ :host([size="md"]) {
39
+ --spinner-size: 2rem;
40
+ --spinner-border-width: 3px;
41
+ }
42
+ :host([size="lg"]) {
43
+ --spinner-size: 3rem;
44
+ --spinner-border-width: 4px;
45
+ }
46
+ :host([size="xl"]) {
47
+ --spinner-size: 4rem;
48
+ --spinner-border-width: 5px;
49
+ }
50
+ /* Spinner variant */
51
+ .spinner {
52
+ width: var(--spinner-size);
53
+ height: var(--spinner-size);
54
+ border: var(--spinner-border-width) solid var(--spinner-track-color);
55
+ border-top-color: var(--spinner-color);
56
+ border-radius: 50%;
57
+ animation: spin 0.8s linear infinite;
58
+ }
59
+ @keyframes spin {
60
+ to { transform: rotate(360deg); }
61
+ }
62
+ /* Dots variant */
63
+ .dots {
64
+ display: flex;
65
+ gap: calc(var(--spinner-size) * 0.2);
66
+ }
67
+ .dots span {
68
+ width: calc(var(--spinner-size) * 0.3);
69
+ height: calc(var(--spinner-size) * 0.3);
70
+ background: var(--spinner-color);
71
+ border-radius: 50%;
72
+ animation: bounce 1.4s ease-in-out infinite both;
73
+ }
74
+ .dots span:nth-child(1) { animation-delay: -0.32s; }
75
+ .dots span:nth-child(2) { animation-delay: -0.16s; }
76
+ .dots span:nth-child(3) { animation-delay: 0s; }
77
+ @keyframes bounce {
78
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
79
+ 40% { transform: scale(1); opacity: 1; }
80
+ }
81
+ /* Bars variant */
82
+ .bars {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: calc(var(--spinner-size) * 0.1);
86
+ height: var(--spinner-size);
87
+ }
88
+ .bars span {
89
+ width: calc(var(--spinner-size) * 0.15);
90
+ height: 60%;
91
+ background: var(--spinner-color);
92
+ border-radius: calc(var(--spinner-size) * 0.05);
93
+ animation: bars 1.2s ease-in-out infinite;
94
+ }
95
+ .bars span:nth-child(1) { animation-delay: -0.24s; }
96
+ .bars span:nth-child(2) { animation-delay: -0.12s; }
97
+ .bars span:nth-child(3) { animation-delay: 0s; }
98
+ .bars span:nth-child(4) { animation-delay: 0.12s; }
99
+ @keyframes bars {
100
+ 0%, 40%, 100% { height: 40%; }
101
+ 20% { height: 100%; }
102
+ }
103
+ /* Pulse variant */
104
+ .pulse {
105
+ width: var(--spinner-size);
106
+ height: var(--spinner-size);
107
+ background: var(--spinner-color);
108
+ border-radius: 50%;
109
+ animation: pulse 1.5s ease-in-out infinite;
110
+ }
111
+ @keyframes pulse {
112
+ 0% { transform: scale(0.8); opacity: 0.5; }
113
+ 50% { transform: scale(1); opacity: 1; }
114
+ 100% { transform: scale(0.8); opacity: 0.5; }
115
+ }
116
+ /* Ring variant */
117
+ .ring {
118
+ width: var(--spinner-size);
119
+ height: var(--spinner-size);
120
+ border: var(--spinner-border-width) solid var(--spinner-color);
121
+ border-radius: 50%;
122
+ animation: ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
123
+ }
124
+ @keyframes ring {
125
+ 0% { transform: rotate(0deg); border-color: var(--spinner-color) transparent transparent transparent; }
126
+ 25% { border-color: transparent var(--spinner-color) transparent transparent; }
127
+ 50% { border-color: transparent transparent var(--spinner-color) transparent; }
128
+ 75% { border-color: transparent transparent transparent var(--spinner-color); }
129
+ 100% { transform: rotate(360deg); border-color: var(--spinner-color) transparent transparent transparent; }
130
+ }
131
+ `;
132
+
133
+ /*
134
+ Rendering
135
+ */
136
+ render() {
137
+ switch(this.variant) {
138
+ case 'dots':
139
+ return html`<div class="dots"><span></span><span></span><span></span></div>`;
140
+ case 'bars':
141
+ return html`<div class="bars"><span></span><span></span><span></span><span></span></div>`;
142
+ case 'pulse':
143
+ return html`<div class="pulse"></div>`;
144
+ case 'ring':
145
+ return html`<div class="ring"></div>`;
146
+ default:
147
+ return html`<div class="spinner"></div>`;
148
+ }
149
+ }
150
+ }
151
+
152
+ customElements.define('k-spinner', Spinner);