@mrintel/villain-ui 0.3.0 → 0.7.1

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 (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3490 -1296
  3. package/dist/components/buttons/Button.svelte +27 -33
  4. package/dist/components/buttons/Button.svelte.d.ts +4 -1
  5. package/dist/components/buttons/ButtonGroup.svelte +17 -30
  6. package/dist/components/buttons/FloatingActionButton.svelte +20 -44
  7. package/dist/components/buttons/FloatingActionButton.svelte.d.ts +2 -1
  8. package/dist/components/buttons/IconButton.svelte +23 -53
  9. package/dist/components/buttons/IconButton.svelte.d.ts +2 -1
  10. package/dist/components/buttons/LinkButton.svelte +24 -37
  11. package/dist/components/buttons/LinkButton.svelte.d.ts +4 -1
  12. package/dist/components/buttons/buttonClasses.d.ts +5 -0
  13. package/dist/components/buttons/buttonClasses.js +8 -3
  14. package/dist/components/cards/Card.svelte +54 -46
  15. package/dist/components/cards/Card.svelte.d.ts +9 -2
  16. package/dist/components/cards/Container.svelte +17 -33
  17. package/dist/components/cards/Divider.svelte +36 -52
  18. package/dist/components/cards/Divider.svelte.d.ts +2 -0
  19. package/dist/components/cards/Grid.svelte +55 -44
  20. package/dist/components/cards/Panel.svelte +18 -32
  21. package/dist/components/cards/Panel.svelte.d.ts +2 -1
  22. package/dist/components/cards/SectionHeader.svelte +24 -38
  23. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  24. package/dist/components/data/Avatar.svelte +48 -67
  25. package/dist/components/data/Badge.svelte +45 -32
  26. package/dist/components/data/Badge.svelte.d.ts +7 -1
  27. package/dist/components/data/CalendarGrid.svelte +433 -0
  28. package/dist/components/data/CalendarGrid.svelte.d.ts +25 -0
  29. package/dist/components/data/CalendarGrid.types.d.ts +7 -0
  30. package/dist/components/data/CalendarGrid.types.js +1 -0
  31. package/dist/components/data/CodeBlock.svelte +119 -121
  32. package/dist/components/data/CodeBlock.svelte.d.ts +8 -0
  33. package/dist/components/data/List.svelte +87 -64
  34. package/dist/components/data/List.svelte.d.ts +7 -0
  35. package/dist/components/data/Pagination.svelte +121 -123
  36. package/dist/components/data/Pagination.svelte.d.ts +5 -0
  37. package/dist/components/data/Sparkline.svelte +117 -0
  38. package/dist/components/data/Sparkline.svelte.d.ts +43 -0
  39. package/dist/components/data/Stat.svelte +92 -103
  40. package/dist/components/data/Table.svelte +443 -76
  41. package/dist/components/data/Table.svelte.d.ts +23 -2
  42. package/dist/components/data/Table.types.d.ts +14 -0
  43. package/dist/components/data/Table.types.js +1 -0
  44. package/dist/components/data/Tag.svelte +51 -53
  45. package/dist/components/data/Tag.svelte.d.ts +5 -1
  46. package/dist/components/data/index.d.ts +4 -0
  47. package/dist/components/data/index.js +2 -0
  48. package/dist/components/forms/Checkbox.svelte +39 -51
  49. package/dist/components/forms/Checkbox.svelte.d.ts +3 -1
  50. package/dist/components/forms/DatePicker.svelte +61 -0
  51. package/dist/components/forms/DatePicker.svelte.d.ts +15 -0
  52. package/dist/components/forms/DateTimePicker.svelte +63 -0
  53. package/dist/components/forms/DateTimePicker.svelte.d.ts +16 -0
  54. package/dist/components/forms/FileUpload.svelte +136 -164
  55. package/dist/components/forms/FileUpload.svelte.d.ts +1 -0
  56. package/dist/components/forms/Input.svelte +284 -57
  57. package/dist/components/forms/Input.svelte.d.ts +10 -3
  58. package/dist/components/forms/InputGroup.svelte +7 -7
  59. package/dist/components/forms/RadioGroup.svelte +77 -87
  60. package/dist/components/forms/RadioGroup.svelte.d.ts +3 -1
  61. package/dist/components/forms/RangeSlider.svelte +90 -116
  62. package/dist/components/forms/Select.svelte +106 -71
  63. package/dist/components/forms/Select.svelte.d.ts +3 -1
  64. package/dist/components/forms/Step.svelte +25 -0
  65. package/dist/components/forms/Step.svelte.d.ts +12 -0
  66. package/dist/components/forms/StepContext.d.ts +3 -0
  67. package/dist/components/forms/StepContext.js +5 -0
  68. package/dist/components/forms/Stepper.types.d.ts +37 -0
  69. package/dist/components/forms/Stepper.types.js +1 -0
  70. package/dist/components/forms/StepperForm.svelte +183 -0
  71. package/dist/components/forms/StepperForm.svelte.d.ts +17 -0
  72. package/dist/components/forms/Switch.svelte +44 -56
  73. package/dist/components/forms/Switch.svelte.d.ts +3 -1
  74. package/dist/components/forms/Textarea.svelte +52 -57
  75. package/dist/components/forms/Textarea.svelte.d.ts +3 -1
  76. package/dist/components/forms/TimePicker.svelte +63 -0
  77. package/dist/components/forms/TimePicker.svelte.d.ts +16 -0
  78. package/dist/components/forms/formClasses.d.ts +3 -0
  79. package/dist/components/forms/formClasses.js +3 -0
  80. package/dist/components/forms/index.d.ts +6 -0
  81. package/dist/components/forms/index.js +5 -0
  82. package/dist/components/navigation/Breadcrumbs.svelte +56 -59
  83. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +1 -0
  84. package/dist/components/navigation/ContextMenu.svelte +133 -83
  85. package/dist/components/navigation/ContextMenu.svelte.d.ts +8 -1
  86. package/dist/components/navigation/DropdownMenu.svelte +139 -80
  87. package/dist/components/navigation/DropdownMenu.svelte.d.ts +8 -1
  88. package/dist/components/navigation/Menu.svelte +72 -48
  89. package/dist/components/navigation/Navbar.svelte +111 -32
  90. package/dist/components/navigation/Navbar.svelte.d.ts +6 -0
  91. package/dist/components/navigation/Sidebar.svelte +236 -35
  92. package/dist/components/navigation/Sidebar.svelte.d.ts +2 -0
  93. package/dist/components/navigation/Stepper.svelte +204 -0
  94. package/dist/components/navigation/Stepper.svelte.d.ts +34 -0
  95. package/dist/components/navigation/Tabs.svelte +86 -54
  96. package/dist/components/navigation/Tabs.svelte.d.ts +5 -1
  97. package/dist/components/navigation/index.d.ts +1 -0
  98. package/dist/components/navigation/index.js +1 -0
  99. package/dist/components/overlays/Alert.svelte +81 -99
  100. package/dist/components/overlays/Alert.svelte.d.ts +5 -1
  101. package/dist/components/overlays/CommandPalette.svelte +182 -217
  102. package/dist/components/overlays/Drawer.svelte +158 -167
  103. package/dist/components/overlays/Drawer.svelte.d.ts +3 -1
  104. package/dist/components/overlays/Dropdown.svelte +62 -30
  105. package/dist/components/overlays/Dropdown.svelte.d.ts +2 -0
  106. package/dist/components/overlays/Modal.svelte +125 -130
  107. package/dist/components/overlays/Modal.svelte.d.ts +3 -1
  108. package/dist/components/overlays/Popover.svelte +106 -131
  109. package/dist/components/overlays/ProgressBar.svelte +29 -45
  110. package/dist/components/overlays/SkeletonLoader.svelte +66 -82
  111. package/dist/components/overlays/Spinner.svelte +33 -43
  112. package/dist/components/overlays/Toast.svelte +111 -140
  113. package/dist/components/overlays/Toast.svelte.d.ts +3 -0
  114. package/dist/components/overlays/Tooltip.svelte +94 -115
  115. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -1
  116. package/dist/components/typography/Code.svelte +10 -14
  117. package/dist/components/typography/Heading.svelte +15 -22
  118. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  119. package/dist/components/typography/Text.svelte +21 -24
  120. package/dist/components/typography/Text.svelte.d.ts +2 -1
  121. package/dist/components/utilities/Accordion.svelte +54 -67
  122. package/dist/components/utilities/Accordion.svelte.d.ts +4 -1
  123. package/dist/components/utilities/Carousel.svelte +124 -152
  124. package/dist/components/utilities/Collapse.svelte +46 -60
  125. package/dist/components/utilities/Hero.svelte +42 -0
  126. package/dist/components/utilities/Hero.svelte.d.ts +10 -0
  127. package/dist/components/utilities/Portal.svelte +47 -72
  128. package/dist/components/utilities/ScrollArea.svelte +33 -41
  129. package/dist/components/utilities/SystemConsole.svelte +310 -0
  130. package/dist/components/utilities/SystemConsole.svelte.d.ts +20 -0
  131. package/dist/components/utilities/SystemInterface.svelte +726 -0
  132. package/dist/components/utilities/SystemInterface.svelte.d.ts +19 -0
  133. package/dist/components/utilities/index.d.ts +4 -0
  134. package/dist/components/utilities/index.js +3 -0
  135. package/dist/components/utilities/utilities.types.d.ts +46 -0
  136. package/dist/components/utilities/utilities.types.js +4 -0
  137. package/dist/index.d.ts +57 -5
  138. package/dist/index.js +5 -5
  139. package/dist/theme.css +2889 -218
  140. package/package.json +83 -76
@@ -1,130 +1,125 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { createId } from '../../lib/internal/id.js';
4
-
5
- interface Props {
6
- open?: boolean;
7
- title?: string;
8
- size?: 'sm' | 'md' | 'lg' | 'xl';
9
- closeOnBackdrop?: boolean;
10
- closeOnEscape?: boolean;
11
- children?: Snippet;
12
- footer?: Snippet;
13
- }
14
-
15
- let {
16
- open = $bindable(false),
17
- title,
18
- size = 'md',
19
- closeOnBackdrop = true,
20
- closeOnEscape = true,
21
- children,
22
- footer
23
- }: Props = $props();
24
-
25
- let modalElement = $state<HTMLDivElement>();
26
- let previousFocus = $state<HTMLElement | null>(null);
27
-
28
- const sizeClasses = {
29
- sm: 'max-w-[28rem]',
30
- md: 'max-w-[36rem]',
31
- lg: 'max-w-[48rem]',
32
- xl: 'max-w-[64rem]'
33
- };
34
-
35
- const titleId = createId('modal-title');
36
-
37
- function handleClose() {
38
- open = false;
39
- }
40
-
41
- function handleBackdropClick(event: MouseEvent) {
42
- if (closeOnBackdrop && event.target === event.currentTarget) {
43
- handleClose();
44
- }
45
- }
46
-
47
- function handleEscape(event: KeyboardEvent) {
48
- if (closeOnEscape && event.key === 'Escape') {
49
- handleClose();
50
- }
51
- }
52
-
53
- $effect(() => {
54
- if (typeof document === 'undefined') return;
55
-
56
- if (open) {
57
- // Store previous focus
58
- previousFocus = document.activeElement as HTMLElement;
59
-
60
- // Prevent body scroll
61
- document.body.style.overflow = 'hidden';
62
-
63
- // Add event listeners
64
- document.addEventListener('keydown', handleEscape);
65
-
66
- // Focus first interactive element
67
- requestAnimationFrame(() => {
68
- const firstInteractive = modalElement?.querySelector<HTMLElement>(
69
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
70
- );
71
- firstInteractive?.focus();
72
- });
73
-
74
- return () => {
75
- // Restore body scroll
76
- document.body.style.overflow = '';
77
-
78
- // Remove event listeners
79
- document.removeEventListener('keydown', handleEscape);
80
-
81
- // Restore previous focus
82
- previousFocus?.focus();
83
- };
84
- }
85
- });
86
- </script>
87
-
88
- {#if open}
89
- <div
90
- class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-overlay backdrop-blur-md animate-[fade-in_0.2s_var(--ease-luxe)]"
91
- onclick={handleBackdropClick}
92
- role="presentation"
93
- >
94
- <div
95
- bind:this={modalElement}
96
- class="glass-panel rounded-xl shadow-deep w-full {sizeClasses[size]} animate-[fade-up_0.3s_var(--ease-luxe)] flex flex-col max-h-[90vh]"
97
- role="dialog"
98
- aria-modal="true"
99
- aria-labelledby={title ? titleId : undefined}
100
- >
101
- {#if title}
102
- <div class="flex items-center justify-between p-6 border-b border-border">
103
- <h2 id={titleId} class="text-xl font-semibold text-text">
104
- {title}
105
- </h2>
106
- <button
107
- type="button"
108
- onclick={handleClose}
109
- class="text-text-soft hover:text-text transition-colors"
110
- aria-label="Close modal"
111
- >
112
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
113
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
114
- </svg>
115
- </button>
116
- </div>
117
- {/if}
118
-
119
- <div class="flex-1 overflow-y-auto p-6 max-h-[70vh]" style="scrollbar-width: thin; scrollbar-color: var(--color-accent) var(--color-base-3);">
120
- {@render children?.()}
121
- </div>
122
-
123
- {#if footer}
124
- <div class="flex items-center justify-end gap-3 p-6 border-t border-border">
125
- {@render footer?.()}
126
- </div>
127
- {/if}
128
- </div>
129
- </div>
130
- {/if}
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ let { open = $bindable(false), title, size = 'md', closeOnBackdrop = true, closeOnEscape = true, children, footer, iconBefore, class: className = '' } = $props();
3
+ let modalElement = $state();
4
+ let previousFocus = $state(null);
5
+ const sizeClasses = {
6
+ sm: 'max-w-[28rem]',
7
+ md: 'max-w-[36rem]',
8
+ lg: 'max-w-[48rem]',
9
+ xl: 'max-w-[64rem]'
10
+ };
11
+ const titleId = createId('modal-title');
12
+ const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"], summary, details, audio[controls], video[controls]';
13
+ function handleClose() {
14
+ open = false;
15
+ }
16
+ function handleBackdropClick(event) {
17
+ if (closeOnBackdrop && event.target === event.currentTarget) {
18
+ handleClose();
19
+ }
20
+ }
21
+ function handleEscape(event) {
22
+ if (closeOnEscape && event.key === 'Escape') {
23
+ handleClose();
24
+ }
25
+ }
26
+ function handleFocusTrap(event) {
27
+ if (event.key !== 'Tab' || !modalElement)
28
+ return;
29
+ const focusableElements = Array.from(modalElement.querySelectorAll(focusableSelector));
30
+ if (focusableElements.length === 0)
31
+ return;
32
+ const firstFocusable = focusableElements[0];
33
+ const lastFocusable = focusableElements[focusableElements.length - 1];
34
+ if (event.shiftKey) {
35
+ // Shift+Tab: moving backwards
36
+ if (document.activeElement === firstFocusable) {
37
+ event.preventDefault();
38
+ lastFocusable.focus();
39
+ }
40
+ }
41
+ else {
42
+ // Tab: moving forwards
43
+ if (document.activeElement === lastFocusable) {
44
+ event.preventDefault();
45
+ firstFocusable.focus();
46
+ }
47
+ }
48
+ }
49
+ $effect(() => {
50
+ if (typeof document === 'undefined')
51
+ return;
52
+ if (open) {
53
+ // Store previous focus
54
+ previousFocus = document.activeElement;
55
+ // Prevent body scroll
56
+ document.body.style.overflow = 'hidden';
57
+ // Add event listeners
58
+ document.addEventListener('keydown', handleEscape);
59
+ document.addEventListener('keydown', handleFocusTrap);
60
+ // Focus first interactive element
61
+ requestAnimationFrame(() => {
62
+ const firstInteractive = modalElement?.querySelector(focusableSelector);
63
+ firstInteractive?.focus();
64
+ });
65
+ return () => {
66
+ // Restore body scroll
67
+ document.body.style.overflow = '';
68
+ // Remove event listeners
69
+ document.removeEventListener('keydown', handleEscape);
70
+ document.removeEventListener('keydown', handleFocusTrap);
71
+ // Restore previous focus
72
+ previousFocus?.focus();
73
+ };
74
+ }
75
+ });
76
+ </script>
77
+
78
+ {#if open}
79
+ <div
80
+ class="fixed inset-0 z-[var(--z-50)] flex items-center justify-center p-4 bg-overlay backdrop-blur-md animate-[fade-in_0.2s_var(--ease-luxe)]"
81
+ onclick={handleBackdropClick}
82
+ role="presentation"
83
+ >
84
+ <div
85
+ bind:this={modalElement}
86
+ class="panel-floating rounded-[var(--radius-lg)] shadow-deep w-full {sizeClasses[size]} {className} animate-[fade-up_0.3s_var(--ease-luxe)] flex flex-col max-h-[90vh]"
87
+ role="dialog"
88
+ aria-modal="true"
89
+ aria-labelledby={title ? titleId : undefined}
90
+ >
91
+ {#if title}
92
+ <div class="flex items-center justify-between p-8 border-b border-border">
93
+ <h2 id={titleId} class="text-xl font-semibold text-text flex items-center gap-3">
94
+ {#if iconBefore}
95
+ <span class="inline-flex items-center justify-center">
96
+ {@render iconBefore()}
97
+ </span>
98
+ {/if}
99
+ {title}
100
+ </h2>
101
+ <button
102
+ type="button"
103
+ onclick={handleClose}
104
+ class="text-text-soft hover:text-text transition-colors"
105
+ aria-label="Close modal"
106
+ >
107
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
108
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
109
+ </svg>
110
+ </button>
111
+ </div>
112
+ {/if}
113
+
114
+ <div class="flex-1 overflow-y-auto p-8 max-h-[70vh]" style="scrollbar-width: thin; scrollbar-color: var(--color-accent) var(--color-base-3);">
115
+ {@render children?.()}
116
+ </div>
117
+
118
+ {#if footer}
119
+ <div class="flex items-center justify-end gap-4 p-8 border-t border-border">
120
+ {@render footer?.()}
121
+ </div>
122
+ {/if}
123
+ </div>
124
+ </div>
125
+ {/if}
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- interface Props {
2
+ export interface Props {
3
3
  open?: boolean;
4
4
  title?: string;
5
5
  size?: 'sm' | 'md' | 'lg' | 'xl';
@@ -7,6 +7,8 @@ interface Props {
7
7
  closeOnEscape?: boolean;
8
8
  children?: Snippet;
9
9
  footer?: Snippet;
10
+ iconBefore?: Snippet;
11
+ class?: string;
10
12
  }
11
13
  declare const Modal: import("svelte").Component<Props, {}, "open">;
12
14
  type Modal = ReturnType<typeof Modal>;
@@ -1,131 +1,106 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import { createId } from '../../lib/internal/id.js';
4
-
5
- interface Props {
6
- open?: boolean;
7
- placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'right';
8
- closeOnClickOutside?: boolean;
9
- trigger?: Snippet;
10
- children?: Snippet;
11
- }
12
-
13
- let {
14
- open = $bindable(false),
15
- placement = 'bottom',
16
- closeOnClickOutside = true,
17
- trigger,
18
- children
19
- }: Props = $props();
20
-
21
- let popoverElement = $state<HTMLDivElement>();
22
- let wrapperElement = $state<HTMLDivElement>();
23
- let actualPlacement = $state(placement);
24
-
25
- const popoverId = createId('popover');
26
-
27
- const placementClasses = {
28
- 'top': 'bottom-full left-1/2 -translate-x-1/2 mb-2',
29
- 'top-start': 'bottom-full left-0 mb-2',
30
- 'top-end': 'bottom-full right-0 mb-2',
31
- 'bottom': 'top-full left-1/2 -translate-x-1/2 mt-2',
32
- 'bottom-start': 'top-full left-0 mt-2',
33
- 'bottom-end': 'top-full right-0 mt-2',
34
- 'left': 'right-full top-1/2 -translate-y-1/2 mr-2',
35
- 'right': 'left-full top-1/2 -translate-y-1/2 ml-2'
36
- };
37
-
38
- const oppositePlacement = {
39
- 'top': 'bottom',
40
- 'top-start': 'bottom-start',
41
- 'top-end': 'bottom-end',
42
- 'bottom': 'top',
43
- 'bottom-start': 'top-start',
44
- 'bottom-end': 'top-end',
45
- 'left': 'right',
46
- 'right': 'left'
47
- } as const;
48
-
49
- function toggleOpen() {
50
- open = !open;
51
- }
52
-
53
- function handleClickOutside(event: MouseEvent) {
54
- if (closeOnClickOutside && popoverElement && wrapperElement && !wrapperElement.contains(event.target as Node)) {
55
- open = false;
56
- }
57
- }
58
-
59
- function handleEscape(event: KeyboardEvent) {
60
- if (event.key === 'Escape') {
61
- open = false;
62
- }
63
- }
64
-
65
- // Reset actualPlacement when visibility changes
66
- $effect(() => {
67
- if (!open) {
68
- actualPlacement = placement;
69
- }
70
- });
71
-
72
- // Check viewport bounds and flip placement if needed
73
- $effect(() => {
74
- if (typeof window === 'undefined') return;
75
-
76
- if (open && popoverElement) {
77
- const rect = popoverElement.getBoundingClientRect();
78
- const viewportWidth = window.innerWidth;
79
- const viewportHeight = window.innerHeight;
80
-
81
- // Determine if current placement overflows and flip if needed
82
- if (actualPlacement.startsWith('top') && rect.top < 0) {
83
- actualPlacement = oppositePlacement[actualPlacement] || placement;
84
- } else if (actualPlacement.startsWith('bottom') && rect.bottom > viewportHeight) {
85
- actualPlacement = oppositePlacement[actualPlacement] || placement;
86
- } else if (actualPlacement === 'left' && rect.left < 0) {
87
- actualPlacement = 'right';
88
- } else if (actualPlacement === 'right' && rect.right > viewportWidth) {
89
- actualPlacement = 'left';
90
- }
91
- }
92
- });
93
-
94
- $effect(() => {
95
- if (typeof document === 'undefined') return;
96
-
97
- if (open) {
98
- document.addEventListener('click', handleClickOutside);
99
- document.addEventListener('keydown', handleEscape);
100
-
101
- return () => {
102
- document.removeEventListener('click', handleClickOutside);
103
- document.removeEventListener('keydown', handleEscape);
104
- };
105
- }
106
- });
107
- </script>
108
-
109
- <div bind:this={wrapperElement} class="relative inline-block">
110
- <button
111
- type="button"
112
- onclick={toggleOpen}
113
- aria-haspopup="true"
114
- aria-expanded={open}
115
- aria-controls={open ? popoverId : undefined}
116
- class="bg-transparent border-none p-0 cursor-pointer"
117
- >
118
- {@render trigger?.()}
119
- </button>
120
-
121
- {#if open}
122
- <div
123
- bind:this={popoverElement}
124
- id={popoverId}
125
- class="absolute {placementClasses[actualPlacement]} z-50 glass-panel rounded-lg shadow-deep animate-[fade-up_0.2s_var(--ease-luxe)]"
126
- role="dialog"
127
- >
128
- {@render children?.()}
129
- </div>
130
- {/if}
131
- </div>
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ let { open = $bindable(false), placement = 'bottom', closeOnClickOutside = true, trigger, children } = $props();
3
+ let popoverElement = $state();
4
+ let wrapperElement = $state();
5
+ let actualPlacement = $state(placement);
6
+ const popoverId = createId('popover');
7
+ const placementClasses = {
8
+ 'top': 'bottom-full left-1/2 -translate-x-1/2 mb-2',
9
+ 'top-start': 'bottom-full left-0 mb-2',
10
+ 'top-end': 'bottom-full right-0 mb-2',
11
+ 'bottom': 'top-full left-1/2 -translate-x-1/2 mt-2',
12
+ 'bottom-start': 'top-full left-0 mt-2',
13
+ 'bottom-end': 'top-full right-0 mt-2',
14
+ 'left': 'right-full top-1/2 -translate-y-1/2 mr-2',
15
+ 'right': 'left-full top-1/2 -translate-y-1/2 ml-2'
16
+ };
17
+ const oppositePlacement = {
18
+ 'top': 'bottom',
19
+ 'top-start': 'bottom-start',
20
+ 'top-end': 'bottom-end',
21
+ 'bottom': 'top',
22
+ 'bottom-start': 'top-start',
23
+ 'bottom-end': 'top-end',
24
+ 'left': 'right',
25
+ 'right': 'left'
26
+ };
27
+ function toggleOpen() {
28
+ open = !open;
29
+ }
30
+ function handleClickOutside(event) {
31
+ if (closeOnClickOutside && popoverElement && wrapperElement && !wrapperElement.contains(event.target)) {
32
+ open = false;
33
+ }
34
+ }
35
+ function handleEscape(event) {
36
+ if (event.key === 'Escape') {
37
+ open = false;
38
+ }
39
+ }
40
+ // Reset actualPlacement when visibility changes
41
+ $effect(() => {
42
+ if (!open) {
43
+ actualPlacement = placement;
44
+ }
45
+ });
46
+ // Check viewport bounds and flip placement if needed
47
+ $effect(() => {
48
+ if (typeof window === 'undefined')
49
+ return;
50
+ if (open && popoverElement) {
51
+ const rect = popoverElement.getBoundingClientRect();
52
+ const viewportWidth = window.innerWidth;
53
+ const viewportHeight = window.innerHeight;
54
+ // Determine if current placement overflows and flip if needed
55
+ if (actualPlacement.startsWith('top') && rect.top < 0) {
56
+ actualPlacement = oppositePlacement[actualPlacement] || placement;
57
+ }
58
+ else if (actualPlacement.startsWith('bottom') && rect.bottom > viewportHeight) {
59
+ actualPlacement = oppositePlacement[actualPlacement] || placement;
60
+ }
61
+ else if (actualPlacement === 'left' && rect.left < 0) {
62
+ actualPlacement = 'right';
63
+ }
64
+ else if (actualPlacement === 'right' && rect.right > viewportWidth) {
65
+ actualPlacement = 'left';
66
+ }
67
+ }
68
+ });
69
+ $effect(() => {
70
+ if (typeof document === 'undefined')
71
+ return;
72
+ if (open) {
73
+ document.addEventListener('click', handleClickOutside);
74
+ document.addEventListener('keydown', handleEscape);
75
+ return () => {
76
+ document.removeEventListener('click', handleClickOutside);
77
+ document.removeEventListener('keydown', handleEscape);
78
+ };
79
+ }
80
+ });
81
+ </script>
82
+
83
+ <div bind:this={wrapperElement} class="relative inline-block">
84
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
85
+ <div
86
+ onclick={toggleOpen}
87
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleOpen(); } }}
88
+ aria-haspopup="true"
89
+ aria-expanded={open}
90
+ aria-controls={open ? popoverId : undefined}
91
+ class="inline-block"
92
+ >
93
+ {@render trigger?.()}
94
+ </div>
95
+
96
+ {#if open}
97
+ <div
98
+ bind:this={popoverElement}
99
+ id={popoverId}
100
+ class="absolute {placementClasses[actualPlacement]} z-[var(--z-50)] panel-floating rounded-[var(--radius-lg)] shadow-deep p-4 animate-[fade-up_0.2s_var(--ease-luxe)]"
101
+ role="dialog"
102
+ >
103
+ {@render children?.()}
104
+ </div>
105
+ {/if}
106
+ </div>
@@ -1,45 +1,29 @@
1
- <script lang="ts">
2
- interface Props {
3
- value: number;
4
- max?: number;
5
- size?: 'sm' | 'md' | 'lg';
6
- showLabel?: boolean;
7
- label?: string;
8
- }
9
-
10
- let {
11
- value,
12
- max = 100,
13
- size = 'md',
14
- showLabel = false,
15
- label
16
- }: Props = $props();
17
-
18
- const percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
19
-
20
- const heightClasses = {
21
- sm: 'h-2',
22
- md: 'h-3',
23
- lg: 'h-4'
24
- };
25
- </script>
26
-
27
- <div
28
- class="relative bg-[var(--color-base-3)] border border-[var(--color-border)] rounded-[var(--radius-pill)] overflow-hidden shadow-[var(--shadow-deep)] {heightClasses[size]}"
29
- role="progressbar"
30
- aria-valuenow={value}
31
- aria-valuemin="0"
32
- aria-valuemax={max}
33
- aria-label={label || `${percentage.toFixed(0)}% complete`}
34
- >
35
- <div
36
- class="h-full bg-[var(--color-accent)] accent-glow transition-all duration-500 ease-[var(--ease-luxe)]"
37
- style="width: {percentage}%"
38
- />
39
-
40
- {#if showLabel}
41
- <div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-[var(--color-text)] text-glow">
42
- {label || `${percentage.toFixed(0)}%`}
43
- </div>
44
- {/if}
45
- </div>
1
+ <script lang="ts">"use strict";
2
+ let { value, max = 100, size = 'md', showLabel = false, label } = $props();
3
+ const percentage = $derived(Math.min(100, Math.max(0, (value / max) * 100)));
4
+ const heightClasses = {
5
+ sm: 'h-2',
6
+ md: 'h-3',
7
+ lg: 'h-4'
8
+ };
9
+ </script>
10
+
11
+ <div
12
+ class="relative bg-[var(--color-base-3)] border border-[var(--color-border)] rounded-[var(--radius-pill)] overflow-hidden shadow-[var(--shadow-deep)] {heightClasses[size]}"
13
+ role="progressbar"
14
+ aria-valuenow={value}
15
+ aria-valuemin="0"
16
+ aria-valuemax={max}
17
+ aria-label={label || `${percentage.toFixed(0)}% complete`}
18
+ >
19
+ <div
20
+ class="h-full bg-[var(--color-accent)] accent-glow transition-all duration-500 ease-[var(--ease-luxe)]"
21
+ style="width: {percentage}%"
22
+ />
23
+
24
+ {#if showLabel}
25
+ <div class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-[var(--color-text)] text-glow">
26
+ {label || `${percentage.toFixed(0)}%`}
27
+ </div>
28
+ {/if}
29
+ </div>