sveltacular 1.0.18 → 1.0.21

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 (42) hide show
  1. package/README.md +1 -2
  2. package/dist/forms/bool-box/bool-box.svelte +392 -13
  3. package/dist/forms/bool-box/bool-box.svelte.d.ts +2 -0
  4. package/dist/forms/bool-box/index.d.ts +2 -0
  5. package/dist/forms/bool-box/index.js +2 -0
  6. package/dist/forms/date-box/date-box.svelte +104 -53
  7. package/dist/forms/date-box/date-box.svelte.d.ts +8 -3
  8. package/dist/forms/dimension-box/dimension-box.svelte +15 -50
  9. package/dist/forms/file-box/file-box.svelte +7 -25
  10. package/dist/forms/form-input-wrapper/form-input-wrapper.svelte +203 -0
  11. package/dist/forms/form-input-wrapper/form-input-wrapper.svelte.d.ts +16 -0
  12. package/dist/forms/form-input-wrapper/index.d.ts +2 -0
  13. package/dist/forms/form-input-wrapper/index.js +2 -0
  14. package/dist/forms/index.d.ts +0 -1
  15. package/dist/forms/index.js +0 -1
  16. package/dist/forms/list-box/list-box.svelte +26 -9
  17. package/dist/forms/list-box/list-box.svelte.d.ts +3 -0
  18. package/dist/forms/money-box/money-box.svelte +104 -65
  19. package/dist/forms/money-box/money-box.svelte.d.ts +6 -0
  20. package/dist/forms/number-box/number-box.svelte +93 -49
  21. package/dist/forms/number-box/number-box.svelte.d.ts +6 -0
  22. package/dist/forms/number-range-box/number-range-box.svelte +22 -58
  23. package/dist/forms/phone-box/phone-box.svelte +101 -38
  24. package/dist/forms/phone-box/phone-box.svelte.d.ts +6 -1
  25. package/dist/forms/slider/slider.svelte +13 -6
  26. package/dist/forms/slider/slider.svelte.d.ts +6 -2
  27. package/dist/forms/tag-box/tag-box.svelte +5 -3
  28. package/dist/forms/text-area/text-area.svelte +22 -2
  29. package/dist/forms/text-area/text-area.svelte.d.ts +4 -0
  30. package/dist/forms/text-box/text-box.svelte +97 -131
  31. package/dist/forms/text-box/text-box.svelte.d.ts +7 -2
  32. package/dist/forms/time-box/time-box.svelte +106 -37
  33. package/dist/forms/time-box/time-box.svelte.d.ts +10 -3
  34. package/dist/forms/url-box/url-box.svelte +26 -5
  35. package/dist/forms/url-box/url-box.svelte.d.ts +7 -1
  36. package/dist/generic/theme-provider/theme-provider-demo.svelte +7 -2
  37. package/dist/navigation/dropdown-button/dropdown-button.svelte +102 -3
  38. package/dist/navigation/dropdown-button/dropdown-manager.svelte.d.ts +47 -0
  39. package/dist/navigation/dropdown-button/dropdown-manager.svelte.js +47 -0
  40. package/dist/tables/table-cell.svelte +2 -0
  41. package/dist/tables/table-row.svelte +1 -0
  42. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  import { Icon } from '../../index.js';
4
4
  import { hasContext } from 'svelte';
5
5
  import { uniqueId } from '../../helpers/unique-id.js';
6
+ import { dropdownManager } from './dropdown-manager.svelte.js';
6
7
 
7
8
  let {
8
9
  open = $bindable(false),
@@ -22,25 +23,112 @@
22
23
  const buttonId = `dropdown-button-${id}`;
23
24
  const menuId = `dropdown-menu-${id}`;
24
25
 
26
+ // DOM element refs for bind:this - these don't need $state() as they're not reactive state
27
+ let containerRef: HTMLDivElement | null = $state(null);
28
+ let menuRef: HTMLDivElement | null = $state(null);
29
+
30
+ let openUpward = $state(false);
31
+
25
32
  const onClick = () => {
26
- open = !open;
33
+ // Use the global manager to handle open/close
34
+ const shouldOpen = dropdownManager.open(id);
35
+ open = shouldOpen;
27
36
  };
28
37
 
38
+ // Sync with global manager state
39
+ $effect(() => {
40
+ const isOpen = dropdownManager.isOpen(id);
41
+ if (isOpen !== open) {
42
+ open = isOpen;
43
+ }
44
+ });
45
+
46
+ // Close this dropdown when another one opens
47
+ $effect(() => {
48
+ if (!dropdownManager.isOpen(id) && open) {
49
+ open = false;
50
+ }
51
+ });
52
+
29
53
  const handleKeyDown = (e: KeyboardEvent) => {
30
54
  if (e.key === 'Escape' && open) {
31
55
  e.preventDefault();
56
+ dropdownManager.close(id);
32
57
  open = false;
33
58
  // Return focus to button
34
59
  document.getElementById(buttonId)?.focus();
35
60
  }
36
61
  };
37
62
 
63
+ // Handle clicks outside dropdown
64
+ $effect(() => {
65
+ if (!open) return;
66
+
67
+ const handleClickOutside = (e: MouseEvent) => {
68
+ if (containerRef && !containerRef.contains(e.target as Node)) {
69
+ dropdownManager.close(id);
70
+ open = false;
71
+ }
72
+ };
73
+
74
+ // Add listener after a small delay to avoid immediate closure from the opening click
75
+ const timeoutId = setTimeout(() => {
76
+ document.addEventListener('click', handleClickOutside);
77
+ }, 0);
78
+
79
+ return () => {
80
+ clearTimeout(timeoutId);
81
+ document.removeEventListener('click', handleClickOutside);
82
+ };
83
+ });
84
+
85
+ // Cleanup on unmount
86
+ $effect(() => {
87
+ return () => {
88
+ dropdownManager.close(id);
89
+ };
90
+ });
91
+
92
+ // Detect if menu should open upward to prevent overflow
93
+ $effect(() => {
94
+ if (open && containerRef && menuRef) {
95
+ const containerRect = containerRef.getBoundingClientRect();
96
+ const menuHeight = menuRef.offsetHeight;
97
+
98
+ // Check both viewport and any scrollable parent container
99
+ let spaceBelow = window.innerHeight - containerRect.bottom;
100
+ let spaceAbove = containerRect.top;
101
+
102
+ // Find the nearest scrollable parent
103
+ let scrollParent = containerRef.parentElement;
104
+ while (scrollParent) {
105
+ const overflowY = window.getComputedStyle(scrollParent).overflowY;
106
+ if (overflowY === 'auto' || overflowY === 'scroll') {
107
+ const parentRect = scrollParent.getBoundingClientRect();
108
+ const parentSpaceBelow = parentRect.bottom - containerRect.bottom;
109
+ const parentSpaceAbove = containerRect.top - parentRect.top;
110
+
111
+ // Use the more restrictive space constraint
112
+ if (parentSpaceBelow < spaceBelow) spaceBelow = parentSpaceBelow;
113
+ if (parentSpaceAbove < spaceAbove) spaceAbove = parentSpaceAbove;
114
+ break;
115
+ }
116
+ scrollParent = scrollParent.parentElement;
117
+ }
118
+
119
+ // If there's not enough space below but more space above, open upward
120
+ openUpward = spaceBelow < menuHeight && spaceAbove > spaceBelow;
121
+ }
122
+ });
123
+
38
124
  let hasText = $derived(text && text.length > 0);
39
125
  </script>
40
126
 
41
127
  <div
128
+ bind:this={containerRef}
42
129
  class="dropdown-button {variant} icon-{icon}"
43
130
  class:open
131
+ class:open-upward={openUpward}
44
132
  role="presentation"
45
133
  onkeydown={handleKeyDown}
46
134
  >
@@ -68,7 +156,7 @@
68
156
  {/if}
69
157
  </button>
70
158
  {#if open}
71
- <div id={menuId} class="menu" role="menu">
159
+ <div bind:this={menuRef} id={menuId} class="menu" role="menu">
72
160
  {@render children?.()}
73
161
  </div>
74
162
  {/if}
@@ -136,13 +224,24 @@
136
224
  border-style: solid;
137
225
  border-width: 1px;
138
226
  border-color: var(--button-secondary-border, #aaa);
139
- z-index: 999;
227
+ z-index: 1000;
140
228
  text-align: center;
229
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
141
230
  }
142
231
  .dropdown-button.open .icon {
143
232
  transition: transform 0.3s linear;
144
233
  transform: rotate(180deg);
145
234
  }
235
+ .dropdown-button.open-upward button {
236
+ border-radius: 0 0 0.5rem 0.5rem;
237
+ }
238
+ .dropdown-button.open-upward .menu {
239
+ top: auto;
240
+ bottom: 100%;
241
+ }
242
+ .dropdown-button.open-upward.open .icon {
243
+ transform: rotate(0deg);
244
+ }
146
245
  .dropdown-button.icon-none button .text {
147
246
  padding-right: 0.5rem;
148
247
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Global dropdown manager to ensure only one dropdown is open at a time
3
+ */
4
+ export declare function createDropdownManager(): {
5
+ /**
6
+ * Register a dropdown as open
7
+ * @param id - Unique identifier for the dropdown
8
+ * @returns true if this dropdown was opened, false if it was closed
9
+ */
10
+ open(id: string): boolean;
11
+ /**
12
+ * Close a specific dropdown
13
+ * @param id - Unique identifier for the dropdown
14
+ */
15
+ close(id: string): void;
16
+ /**
17
+ * Check if a specific dropdown is currently open
18
+ * @param id - Unique identifier for the dropdown
19
+ */
20
+ isOpen(id: string): boolean;
21
+ /**
22
+ * Close all dropdowns
23
+ */
24
+ closeAll(): void;
25
+ };
26
+ export declare const dropdownManager: {
27
+ /**
28
+ * Register a dropdown as open
29
+ * @param id - Unique identifier for the dropdown
30
+ * @returns true if this dropdown was opened, false if it was closed
31
+ */
32
+ open(id: string): boolean;
33
+ /**
34
+ * Close a specific dropdown
35
+ * @param id - Unique identifier for the dropdown
36
+ */
37
+ close(id: string): void;
38
+ /**
39
+ * Check if a specific dropdown is currently open
40
+ * @param id - Unique identifier for the dropdown
41
+ */
42
+ isOpen(id: string): boolean;
43
+ /**
44
+ * Close all dropdowns
45
+ */
46
+ closeAll(): void;
47
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Global dropdown manager to ensure only one dropdown is open at a time
3
+ */
4
+ let openDropdownId = $state(null);
5
+ export function createDropdownManager() {
6
+ return {
7
+ /**
8
+ * Register a dropdown as open
9
+ * @param id - Unique identifier for the dropdown
10
+ * @returns true if this dropdown was opened, false if it was closed
11
+ */
12
+ open(id) {
13
+ if (openDropdownId === id) {
14
+ // Same dropdown clicked again - close it
15
+ openDropdownId = null;
16
+ return false;
17
+ }
18
+ // Close any other open dropdown and open this one
19
+ openDropdownId = id;
20
+ return true;
21
+ },
22
+ /**
23
+ * Close a specific dropdown
24
+ * @param id - Unique identifier for the dropdown
25
+ */
26
+ close(id) {
27
+ if (openDropdownId === id) {
28
+ openDropdownId = null;
29
+ }
30
+ },
31
+ /**
32
+ * Check if a specific dropdown is currently open
33
+ * @param id - Unique identifier for the dropdown
34
+ */
35
+ isOpen(id) {
36
+ return openDropdownId === id;
37
+ },
38
+ /**
39
+ * Close all dropdowns
40
+ */
41
+ closeAll() {
42
+ openDropdownId = null;
43
+ }
44
+ };
45
+ }
46
+ // Create singleton instance
47
+ export const dropdownManager = createDropdownManager();
@@ -56,4 +56,6 @@ td.boolean, td.check {
56
56
  }
57
57
  td.actions {
58
58
  white-space: nowrap;
59
+ position: relative;
60
+ overflow: visible;
59
61
  }</style>
@@ -36,6 +36,7 @@
36
36
  color: var(--table-row-even-fg, #000);
37
37
  border-bottom: solid 1px var(--table-row-even-border, #ddd);
38
38
  transition: background-color 0.15s ease;
39
+ position: relative;
39
40
  }
40
41
 
41
42
  tr:nth-of-type(odd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.18",
3
+ "version": "1.0.21",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",