@invopop/popui 0.1.95 → 0.1.97

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.
@@ -3,6 +3,7 @@
3
3
  import { Icon } from '@steeze-ui/svelte-icon'
4
4
  import { Warning, Failure } from '@invopop/ui-icons'
5
5
  import ProgressBar from './ProgressBar.svelte'
6
+ import ProgressBarCircle from './ProgressBarCircle.svelte'
6
7
 
7
8
  let {
8
9
  label,
@@ -10,43 +11,60 @@
10
11
  total,
11
12
  resetDate = '',
12
13
  icon,
13
- allowOverage = true
14
+ allowOverage = true,
15
+ collapsed = false
14
16
  }: CounterWidgetProps = $props()
15
17
 
16
- let percentage = $derived(total > 0 ? (current / total) * 100 : 0)
18
+ let progress = $derived(total > 0 ? current / total : 0)
19
+ let percentage = $derived(progress * 100)
17
20
  let isOverage = $derived(current > total && allowOverage)
18
21
  let isCritical = $derived(percentage >= 100 || (current > total && !allowOverage))
19
22
  let isWarning = $derived(percentage >= 80 && percentage < 100)
23
+ let circleVariant = $derived.by<'dark' | 'warning' | 'critical'>(() => {
24
+ if (isCritical) return 'critical'
25
+ if (isWarning) return 'warning'
26
+ return 'dark'
27
+ })
20
28
  </script>
21
29
 
22
- <div class="border border-border-inverse rounded-xl w-full py-[5px] pl-2 pr-1.5 flex flex-col">
23
- <div class="flex items-center justify-between text-base font-medium">
24
- <div class="flex items-center gap-1.5">
25
- {#if icon}
26
- <Icon src={icon} class="size-3 text-icon-inverse rounded-xs" />
27
- {/if}
28
- <span class="font-medium text-foreground-inverse">
29
- {label}
30
- </span>
30
+ {#if collapsed}
31
+ <div
32
+ class="size-8 p-1.75 flex items-center justify-center rounded-lg border border-transparent hover:bg-background-selected-inverse"
33
+ title="{label}: {current}/{total}"
34
+ data-counter-widget-collapsed
35
+ >
36
+ <ProgressBarCircle {progress} size={16} variant={circleVariant} />
37
+ </div>
38
+ {:else}
39
+ <div class="border border-border-inverse rounded-xl w-full py-[5px] pl-2 pr-1.5 flex flex-col">
40
+ <div class="flex items-center justify-between text-base font-medium">
41
+ <div class="flex items-center gap-1.5">
42
+ {#if icon}
43
+ <Icon src={icon} class="size-3 text-icon-inverse rounded-xs" />
44
+ {/if}
45
+ <span class="font-medium text-foreground-inverse">
46
+ {label}
47
+ </span>
48
+ </div>
49
+ <div class="flex items-center gap-1.5">
50
+ {#if isCritical && !isOverage}
51
+ <Icon src={Failure} class="size-4 text-icon-critical-inverse" />
52
+ {:else if isWarning && !isOverage}
53
+ <Icon src={Warning} class="size-4 text-icon-warning-inverse" />
54
+ {/if}
55
+ <span class="font-mono text-foreground-inverse">
56
+ {current}<span class="text-white/70">/{total}</span>
57
+ </span>
58
+ </div>
31
59
  </div>
32
- <div class="flex items-center gap-1.5">
33
- {#if isCritical && !isOverage}
34
- <Icon src={Failure} class="size-4 text-icon-critical-inverse" />
35
- {:else if isWarning && !isOverage}
36
- <Icon src={Warning} class="size-4 text-icon-warning-inverse" />
37
- {/if}
38
- <span class="font-mono text-foreground-inverse">
39
- {current}<span class="text-white/70">/{total}</span>
40
- </span>
60
+ <div class="py-2">
61
+ <ProgressBar {percentage} {current} {total} {allowOverage} />
41
62
  </div>
42
- </div>
43
- <div class="py-2">
44
- <ProgressBar {percentage} {current} {total} {allowOverage} />
45
- </div>
46
63
 
47
- {#if resetDate}
48
- <span class="text-sm font-normal text-foreground-inverse-secondary">
49
- {label} reset {resetDate}
50
- </span>
51
- {/if}
52
- </div>
64
+ {#if resetDate}
65
+ <span class="text-sm font-normal text-foreground-inverse-secondary">
66
+ {label} reset {resetDate}
67
+ </span>
68
+ {/if}
69
+ </div>
70
+ {/if}
@@ -1,13 +1,22 @@
1
1
  <script lang="ts">
2
2
  import MenuItem from './MenuItem.svelte'
3
+ import { flip, shift, offset } from 'svelte-floating-ui/dom'
4
+ import { createFloatingActions } from 'svelte-floating-ui'
3
5
  import clsx from 'clsx'
4
6
  import { cn } from './utils.js'
5
- import type { MenuItemProps } from './types.ts'
7
+ import type { MenuItemProps, DrawerOption, AnyProp } from './types.ts'
6
8
  import { Icon, type IconSource } from '@steeze-ui/svelte-icon'
7
- import { ChevronDown, ChevronRight } from '@invopop/ui-icons'
9
+ import { ChevronDown, ChevronRight, FolderL } from '@invopop/ui-icons'
8
10
  import { resolveIcon } from './helpers.js'
11
+ import DrawerContext from './DrawerContext.svelte'
9
12
  import TagBeta from './TagBeta.svelte'
10
13
 
14
+ const [floatingRef, floatingContent] = createFloatingActions({
15
+ strategy: 'absolute',
16
+ placement: 'bottom-start',
17
+ middleware: [offset(-4), flip(), shift()]
18
+ })
19
+
11
20
  let {
12
21
  label = '',
13
22
  url = '',
@@ -15,6 +24,7 @@
15
24
  collapsable = false,
16
25
  open = $bindable(false),
17
26
  active = false,
27
+ collapsedSidebar = false,
18
28
  iconTheme = 'default',
19
29
  icon = undefined,
20
30
  imageUrl = undefined,
@@ -27,19 +37,46 @@
27
37
  }: MenuItemProps = $props()
28
38
 
29
39
  let resolvedIcon: IconSource | undefined = $state()
30
- let itemStyles = $derived(
31
- clsx('flex-1 min-w-0 px-2 py-1.5 h-8', {
40
+ let hovered = $state(false)
41
+ let highlight = $state(false)
42
+ let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null
43
+ let rowStyles = $derived(
44
+ clsx('flex items-center rounded-lg border border-transparent', {
45
+ 'py-1 pr-1': !collapsedSidebar,
46
+ 'pl-2': !collapsedSidebar && !imageUrl,
47
+ 'pl-[10px]': !collapsedSidebar && imageUrl,
48
+ 'gap-1.5': !collapsedSidebar && (action || collapsable),
49
+ 'size-8 p-[7px] justify-center': collapsedSidebar,
50
+ 'bg-background-selected-inverse': active,
51
+ 'hover:bg-background-selected-inverse': !active
52
+ })
53
+ )
54
+ let buttonStyles = $derived(
55
+ clsx('cursor-pointer text-base flex items-center hover:text-white focus:text-white', {
56
+ 'flex-1 min-w-0 h-6': !collapsedSidebar,
57
+ 'group': collapsedSidebar,
32
58
  'text-foreground-inverse font-medium': !isFolderItem,
33
59
  'text-foreground-inverse-secondary': isFolderItem && !active,
34
- 'bg-background-selected-inverse text-white': active,
35
- 'hover:bg-background-selected-inverse': !active
60
+ 'text-white': active
36
61
  })
37
62
  )
63
+ let iconStyles = $derived(
64
+ clsx({ 'group-hover:text-white text-icon-inverse-bold!': collapsedSidebar })
65
+ )
38
66
  let wrapperStyles = $derived(
39
67
  clsx({
40
68
  'group/menu-item ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem
41
69
  })
42
70
  )
71
+ let items = $derived([
72
+ { label, value: url, selected: active, icon: resolvedIcon },
73
+ ...(children || []).map((c) => ({
74
+ label: c.label || '',
75
+ value: c.url || '',
76
+ selected: c.active,
77
+ icon: FolderL
78
+ }))
79
+ ] as DrawerOption[])
43
80
 
44
81
  $effect(() => {
45
82
  resolveIcon(icon).then((res) => (resolvedIcon = res))
@@ -52,25 +89,58 @@
52
89
 
53
90
  onclick?.(url)
54
91
  }
92
+
93
+ function handleClickChild(value: AnyProp) {
94
+ hovered = false
95
+ onclick?.(value as string)
96
+ }
97
+
98
+ function handleHover() {
99
+ highlight = true
100
+ if (leaveHoverTimeout) {
101
+ clearTimeout(leaveHoverTimeout)
102
+ }
103
+ hovered = true
104
+ }
105
+
106
+ function handleBlur() {
107
+ highlight = false
108
+ leaveHoverTimeout = setTimeout(() => {
109
+ hovered = false
110
+ }, 200)
111
+ }
55
112
  </script>
56
113
 
57
114
  <div bind:this={ref} class={cn(wrapperStyles, className)} data-menu-item-root>
58
115
  {#if isFolderItem}
59
116
  <div
60
117
  class={clsx('border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px', {
61
- 'opacity-0 group-hover/menu-item:opacity-100': !active
118
+ 'opacity-0 group-hover/menu-item:opacity-100': !active && !collapsedSidebar,
119
+ 'opacity-0': collapsedSidebar && !highlight && !active
62
120
  })}
63
121
  data-menu-item-tree-indicator
64
122
  ></div>
65
123
  {/if}
66
- <div class={clsx('flex items-center', { 'gap-1.5': action })} data-menu-item-row>
124
+ <div
125
+ class={rowStyles}
126
+ data-menu-item-row
127
+ use:floatingRef
128
+ onmouseenter={collapsedSidebar ? handleHover : undefined}
129
+ onmouseleave={collapsedSidebar ? handleBlur : undefined}
130
+ role={collapsedSidebar ? 'presentation' : undefined}
131
+ >
67
132
  <button
68
133
  onclick={handleClick}
69
134
  title={label}
70
135
  data-menu-item-button
71
- class="cursor-pointer {itemStyles} text-base border border-transparent flex items-center justify-between hover:text-white focus:text-white rounded-lg"
136
+ class={buttonStyles}
72
137
  >
73
- <span class="flex items-center space-x-1.5 min-w-0 flex-1" data-menu-item-content>
138
+ <span
139
+ class={clsx('flex items-center', {
140
+ 'space-x-1.5 min-w-0 flex-1': !collapsedSidebar
141
+ })}
142
+ data-menu-item-content
143
+ >
74
144
  {#if imageUrl}
75
145
  <img
76
146
  src={imageUrl}
@@ -82,41 +152,54 @@
82
152
  <Icon
83
153
  src={resolvedIcon}
84
154
  theme={iconTheme}
85
- class="h-4 w-4 text-icon-inverse"
155
+ class="{iconStyles} h-4 w-4 text-icon-inverse"
86
156
  data-menu-item-icon
87
157
  />
88
158
  {/if}
89
- <span class="truncate tracking-normal" data-menu-item-label>{label}</span>
90
- {#if beta}
91
- <TagBeta />
159
+ {#if !collapsedSidebar}
160
+ <span class="truncate tracking-normal" data-menu-item-label>{label}</span>
161
+ {#if beta}
162
+ <TagBeta />
163
+ {/if}
92
164
  {/if}
93
165
  </span>
94
- {#if collapsable}
95
- <button
96
- class="cursor-pointer"
97
- data-menu-item-chevron
98
- onclick={(e) => {
99
- e.stopPropagation()
100
- open = !open
101
- }}
102
- >
103
- <Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" />
104
- </button>
105
- {/if}
106
166
  </button>
107
- {#if action}
167
+ {#if !collapsedSidebar && action}
108
168
  <span class="shrink-0" data-menu-item-action>
109
169
  {@render action()}
110
170
  </span>
111
171
  {/if}
172
+ {#if !collapsedSidebar && collapsable}
173
+ <button
174
+ class="shrink-0 cursor-pointer"
175
+ data-menu-item-chevron
176
+ onclick={() => (open = !open)}
177
+ >
178
+ <Icon src={open ? ChevronDown : ChevronRight} class="h-4 w-4 text-white-40" />
179
+ </button>
180
+ {/if}
112
181
  </div>
113
- {#if children?.length && (open || !collapsable)}
114
- <ul data-menu-item-children>
115
- {#each children as child}
116
- <li>
117
- <MenuItem {...child} isFolderItem {onclick} />
118
- </li>
119
- {/each}
120
- </ul>
182
+ {#if children?.length}
183
+ {#if collapsedSidebar}
184
+ {#if hovered}
185
+ <div
186
+ use:floatingContent
187
+ role="contentinfo"
188
+ onmouseenter={handleHover}
189
+ onmouseleave={handleBlur}
190
+ class="pt-4 z-30"
191
+ >
192
+ <DrawerContext autofocus onclick={handleClickChild} {items} />
193
+ </div>
194
+ {/if}
195
+ {:else if open || !collapsable}
196
+ <ul data-menu-item-children>
197
+ {#each children as child}
198
+ <li>
199
+ <MenuItem {...child} isFolderItem {onclick} />
200
+ </li>
201
+ {/each}
202
+ </ul>
203
+ {/if}
121
204
  {/if}
122
205
  </div>
@@ -4,13 +4,16 @@
4
4
  let { progress, size, variant = 'default' }: ProgressBarCircleProps = $props()
5
5
 
6
6
  let angle = $derived(360 * progress)
7
- let filledColor = $derived(
8
- variant === 'dark' ? 'var(--color-icon-inverse-bold)' : 'var(--color-icon-selected-default)'
9
- )
7
+ let filledColor = $derived.by(() => {
8
+ if (variant === 'critical') return 'var(--color-background-critical-inverse)'
9
+ if (variant === 'warning') return 'var(--color-background-warning-inverse)'
10
+ if (variant === 'dark') return 'var(--color-icon-inverse-bold)'
11
+ return 'var(--color-icon-selected-default)'
12
+ })
10
13
  let restColor = $derived(
11
- variant === 'dark'
12
- ? 'var(--color-background-selected-inverse-hover)'
13
- : 'var(--color-background-default-tertiary)'
14
+ variant === 'default'
15
+ ? 'var(--color-background-default-tertiary)'
16
+ : 'var(--color-background-selected-inverse-hover)'
14
17
  )
15
18
  let background = $derived(
16
19
  `conic-gradient(${filledColor} 0deg ${angle}deg, ${restColor} ${angle}deg 360deg)`
package/dist/types.d.ts CHANGED
@@ -200,6 +200,7 @@ export interface CounterWidgetProps {
200
200
  resetDate?: string;
201
201
  icon?: IconSource;
202
202
  allowOverage?: boolean;
203
+ collapsed?: boolean;
203
204
  }
204
205
  export interface ProgressBarProps {
205
206
  percentage?: number;
@@ -211,7 +212,7 @@ export interface ProgressBarProps {
211
212
  export interface ProgressBarCircleProps {
212
213
  progress: number;
213
214
  size: number;
214
- variant?: 'default' | 'dark';
215
+ variant?: 'default' | 'dark' | 'warning' | 'critical';
215
216
  }
216
217
  export interface TagProgressProps {
217
218
  progress: number;
@@ -533,6 +534,7 @@ export interface MenuItemProps {
533
534
  collapsable?: boolean;
534
535
  open?: boolean;
535
536
  active?: boolean;
537
+ collapsedSidebar?: boolean;
536
538
  iconTheme?: IconTheme;
537
539
  icon?: IconSource | string | undefined;
538
540
  imageUrl?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@invopop/popui",
3
3
  "license": "MIT",
4
- "version": "0.1.95",
4
+ "version": "0.1.97",
5
5
  "repository": {
6
6
  "url": "https://github.com/invopop/popui"
7
7
  },