@invopop/popui 0.1.96 → 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,28 +37,46 @@
27
37
  }: MenuItemProps = $props()
28
38
 
29
39
  let resolvedIcon: IconSource | undefined = $state()
40
+ let hovered = $state(false)
41
+ let highlight = $state(false)
42
+ let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null
30
43
  let rowStyles = $derived(
31
- clsx('flex items-center rounded-lg border border-transparent p-1', {
32
- 'gap-1.5': action || collapsable,
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,
33
50
  'bg-background-selected-inverse': active,
34
51
  'hover:bg-background-selected-inverse': !active
35
52
  })
36
53
  )
37
54
  let buttonStyles = $derived(
38
- clsx(
39
- 'flex-1 min-w-0 h-6 cursor-pointer text-base flex items-center hover:text-white focus:text-white',
40
- {
41
- 'text-foreground-inverse font-medium': !isFolderItem,
42
- 'text-foreground-inverse-secondary': isFolderItem && !active,
43
- 'text-white': active
44
- }
45
- )
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,
58
+ 'text-foreground-inverse font-medium': !isFolderItem,
59
+ 'text-foreground-inverse-secondary': isFolderItem && !active,
60
+ 'text-white': active
61
+ })
62
+ )
63
+ let iconStyles = $derived(
64
+ clsx({ 'group-hover:text-white text-icon-inverse-bold!': collapsedSidebar })
46
65
  )
47
66
  let wrapperStyles = $derived(
48
67
  clsx({
49
68
  'group/menu-item ml-4 border-l border-white-10 pl-2 pt-0.5 relative': isFolderItem
50
69
  })
51
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[])
52
80
 
53
81
  $effect(() => {
54
82
  resolveIcon(icon).then((res) => (resolvedIcon = res))
@@ -61,25 +89,58 @@
61
89
 
62
90
  onclick?.(url)
63
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
+ }
64
112
  </script>
65
113
 
66
114
  <div bind:this={ref} class={cn(wrapperStyles, className)} data-menu-item-root>
67
115
  {#if isFolderItem}
68
116
  <div
69
117
  class={clsx('border-l border-white h-3 w-px absolute top-3.5 left-0 -m-px', {
70
- '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
71
120
  })}
72
121
  data-menu-item-tree-indicator
73
122
  ></div>
74
123
  {/if}
75
- <div class={rowStyles} 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
+ >
76
132
  <button
77
133
  onclick={handleClick}
78
134
  title={label}
79
135
  data-menu-item-button
80
136
  class={buttonStyles}
81
137
  >
82
- <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
+ >
83
144
  {#if imageUrl}
84
145
  <img
85
146
  src={imageUrl}
@@ -91,22 +152,24 @@
91
152
  <Icon
92
153
  src={resolvedIcon}
93
154
  theme={iconTheme}
94
- class="h-4 w-4 text-icon-inverse"
155
+ class="{iconStyles} h-4 w-4 text-icon-inverse"
95
156
  data-menu-item-icon
96
157
  />
97
158
  {/if}
98
- <span class="truncate tracking-normal" data-menu-item-label>{label}</span>
99
- {#if beta}
100
- <TagBeta />
159
+ {#if !collapsedSidebar}
160
+ <span class="truncate tracking-normal" data-menu-item-label>{label}</span>
161
+ {#if beta}
162
+ <TagBeta />
163
+ {/if}
101
164
  {/if}
102
165
  </span>
103
166
  </button>
104
- {#if action}
167
+ {#if !collapsedSidebar && action}
105
168
  <span class="shrink-0" data-menu-item-action>
106
169
  {@render action()}
107
170
  </span>
108
171
  {/if}
109
- {#if collapsable}
172
+ {#if !collapsedSidebar && collapsable}
110
173
  <button
111
174
  class="shrink-0 cursor-pointer"
112
175
  data-menu-item-chevron
@@ -116,13 +179,27 @@
116
179
  </button>
117
180
  {/if}
118
181
  </div>
119
- {#if children?.length && (open || !collapsable)}
120
- <ul data-menu-item-children>
121
- {#each children as child}
122
- <li>
123
- <MenuItem {...child} isFolderItem {onclick} />
124
- </li>
125
- {/each}
126
- </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}
127
204
  {/if}
128
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.96",
4
+ "version": "0.1.97",
5
5
  "repository": {
6
6
  "url": "https://github.com/invopop/popui"
7
7
  },