@invopop/popui 0.1.99 → 0.1.101

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.
@@ -41,7 +41,7 @@
41
41
  let highlight = $state(false)
42
42
  let leaveHoverTimeout: ReturnType<typeof setTimeout> | null = null
43
43
  let rowStyles = $derived(
44
- clsx('flex items-center rounded-lg border border-transparent', {
44
+ clsx('flex items-center rounded-lg', {
45
45
  'py-1 pr-1': !collapsedSidebar,
46
46
  'pl-2': !collapsedSidebar && !imageUrl,
47
47
  'pl-[10px]': !collapsedSidebar && imageUrl,
@@ -165,7 +165,7 @@
165
165
  </span>
166
166
  </button>
167
167
  {#if !collapsedSidebar && action}
168
- <span class="shrink-0" data-menu-item-action>
168
+ <span class="shrink-0 flex items-center" data-menu-item-action>
169
169
  {@render action()}
170
170
  </span>
171
171
  {/if}
@@ -3,8 +3,9 @@ export declare const SIDEBAR_WIDTH_STORAGE_KEY = "sidebar_width";
3
3
  export declare const SIDEBAR_COOKIE_MAX_AGE: number;
4
4
  export declare const SIDEBAR_WIDTH = "16rem";
5
5
  export declare const SIDEBAR_WIDTH_MOBILE = "18rem";
6
- export declare const SIDEBAR_WIDTH_ICON = "3rem";
6
+ export declare const SIDEBAR_WIDTH_ICON = "3.5rem";
7
+ export declare const SIDEBAR_WIDTH_ICON_PX = 56;
7
8
  export declare const SIDEBAR_KEYBOARD_SHORTCUT = ".";
8
- export declare const SIDEBAR_MIN_WIDTH_PX = 240;
9
+ export declare const SIDEBAR_MIN_WIDTH_PX = 180;
9
10
  export declare const SIDEBAR_MAX_WIDTH_PX = 384;
10
11
  export declare const SIDEBAR_DRAG_THRESHOLD_PX = 4;
@@ -3,8 +3,9 @@ export const SIDEBAR_WIDTH_STORAGE_KEY = 'sidebar_width';
3
3
  export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
4
4
  export const SIDEBAR_WIDTH = '16rem';
5
5
  export const SIDEBAR_WIDTH_MOBILE = '18rem';
6
- export const SIDEBAR_WIDTH_ICON = '3rem';
6
+ export const SIDEBAR_WIDTH_ICON = '3.5rem';
7
+ export const SIDEBAR_WIDTH_ICON_PX = 56;
7
8
  export const SIDEBAR_KEYBOARD_SHORTCUT = '.';
8
- export const SIDEBAR_MIN_WIDTH_PX = 240;
9
+ export const SIDEBAR_MIN_WIDTH_PX = 180;
9
10
  export const SIDEBAR_MAX_WIDTH_PX = 384;
10
11
  export const SIDEBAR_DRAG_THRESHOLD_PX = 4;
@@ -9,11 +9,13 @@ declare class SidebarState {
9
9
  open: boolean;
10
10
  openMobile: boolean;
11
11
  width: string;
12
+ isResizing: boolean;
12
13
  setOpen: SidebarStateProps['setOpen'];
13
14
  state: string;
14
15
  constructor(props: SidebarStateProps);
15
16
  get isMobile(): boolean;
16
17
  setWidth: (px: number) => void;
18
+ resetWidth: () => void;
17
19
  handleShortcutKeydown: (e: KeyboardEvent) => void;
18
20
  setOpenMobile: (value: boolean) => void;
19
21
  toggle: () => boolean | void;
@@ -6,6 +6,7 @@ class SidebarState {
6
6
  open = $derived.by(() => this.props.open());
7
7
  openMobile = $state(false);
8
8
  width = $state(SIDEBAR_WIDTH);
9
+ isResizing = $state(false);
9
10
  setOpen;
10
11
  #isMobile;
11
12
  state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
@@ -34,6 +35,17 @@ class SidebarState {
34
35
  }
35
36
  }
36
37
  };
38
+ resetWidth = () => {
39
+ this.width = SIDEBAR_WIDTH;
40
+ if (typeof localStorage !== 'undefined') {
41
+ try {
42
+ localStorage.removeItem(SIDEBAR_WIDTH_STORAGE_KEY);
43
+ }
44
+ catch {
45
+ // ignore private-mode errors
46
+ }
47
+ }
48
+ };
37
49
  handleShortcutKeydown = (e) => {
38
50
  if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) {
39
51
  e.preventDefault();
@@ -1,20 +1,12 @@
1
1
  <script lang="ts">
2
- import type { HTMLAttributes } from 'svelte/elements'
2
+ import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements'
3
+ import { Tooltip as TooltipPrimitive } from 'bits-ui'
3
4
  import ShortcutWrapper from '../ShortcutWrapper.svelte'
5
+ import TooltipContent from '../tooltip/tooltip-content.svelte'
4
6
  import { cn, type WithElementRef } from '../utils.js'
5
- import { SIDEBAR_DRAG_THRESHOLD_PX } from './constants.js'
7
+ import { SIDEBAR_DRAG_THRESHOLD_PX, SIDEBAR_WIDTH_ICON_PX } from './constants.js'
6
8
  import { useSidebar } from './context.svelte.js'
7
9
 
8
- function portal(node: HTMLElement) {
9
- const target = typeof document !== 'undefined' ? document.body : null
10
- if (target) target.appendChild(node)
11
- return {
12
- destroy() {
13
- if (node.parentNode) node.parentNode.removeChild(node)
14
- }
15
- }
16
- }
17
-
18
10
  let {
19
11
  ref = $bindable(null),
20
12
  class: className,
@@ -28,149 +20,212 @@
28
20
  let dragStartWidthPx = 0
29
21
  let dragMoved = false
30
22
  let dragDirection: 1 | -1 = 1
23
+ let activePointerId: number | null = null
31
24
 
32
- let isHovering = $state(false)
33
25
  let isDragging = $state(false)
34
- let tooltipVisible = $derived(isHovering && !isDragging)
35
- let tooltipX = $state(0)
36
- let tooltipY = $state(0)
37
- let tooltipWidth = $state(0)
38
- let tooltipHeight = $state(0)
26
+ let tooltipOpen = $state(false)
27
+ let cursorX = $state(0)
28
+ let cursorY = $state(0)
39
29
 
30
+ const TOOLTIP_HOVER_DELAY_MS = 700
40
31
  const TOOLTIP_CURSOR_OFFSET = 12
41
- const TOOLTIP_VIEWPORT_PADDING = 8
42
-
43
- let tooltipLeft = $derived.by(() => {
44
- if (tooltipWidth === 0 || typeof window === 'undefined') return tooltipX
45
- const desired = tooltipX - tooltipWidth / 2
46
- const minX = TOOLTIP_VIEWPORT_PADDING
47
- const maxX = window.innerWidth - tooltipWidth - TOOLTIP_VIEWPORT_PADDING
48
- return Math.max(minX, Math.min(maxX, desired))
49
- })
50
32
 
51
- let tooltipTop = $derived.by(() => {
52
- const desired = tooltipY + TOOLTIP_CURSOR_OFFSET
53
- if (tooltipHeight === 0 || typeof window === 'undefined') return desired
54
- const maxY = window.innerHeight - tooltipHeight - TOOLTIP_VIEWPORT_PADDING
55
- return Math.min(maxY, desired)
33
+ let tooltipDelay = $derived(isDragging ? Number.MAX_SAFE_INTEGER : TOOLTIP_HOVER_DELAY_MS)
34
+
35
+ let cursorAnchor = $derived.by(() => {
36
+ const x = cursorX
37
+ const y = cursorY
38
+ return {
39
+ getBoundingClientRect: () => DOMRect.fromRect({ x, y, width: 0, height: 0 })
40
+ }
56
41
  })
57
42
 
58
- function updateTooltipPosition(e: PointerEvent) {
59
- tooltipX = e.clientX
60
- tooltipY = e.clientY
61
- }
43
+ const POST_DRAG_CLICK_GUARD_MS = 250
44
+ let dragEndTime = 0
62
45
 
63
46
  function onPointerEnter(e: PointerEvent) {
64
47
  const sidebarRoot = (e.currentTarget as HTMLElement).closest('[data-slot="sidebar"]')
65
48
  dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
66
- updateTooltipPosition(e)
67
- isHovering = true
49
+ cursorX = e.clientX
50
+ cursorY = e.clientY
68
51
  }
69
52
 
70
- function onPointerLeave() {
71
- isHovering = false
53
+ function onPointerMoveOnTrigger(e: PointerEvent) {
54
+ if (activePointerId !== null) return
55
+ cursorX = e.clientX
56
+ cursorY = e.clientY
57
+ }
58
+
59
+ function endDrag() {
60
+ if (activePointerId !== null) {
61
+ window.removeEventListener('pointermove', onWindowPointerMove)
62
+ window.removeEventListener('pointerup', onWindowPointerUp)
63
+ window.removeEventListener('pointercancel', onWindowPointerUp)
64
+ activePointerId = null
65
+ }
66
+ document.body.style.cursor = ''
67
+ document.body.style.userSelect = ''
68
+ isDragging = false
69
+ sidebar.isResizing = false
70
+ if (dragMoved) {
71
+ dragEndTime = Date.now()
72
+ dragMoved = false
73
+ }
74
+ }
75
+
76
+ function onWindowPointerMove(e: PointerEvent) {
77
+ if (e.pointerId !== activePointerId) return
78
+ cursorX = e.clientX
79
+ cursorY = e.clientY
80
+ const delta = (e.clientX - dragStartX) * dragDirection
81
+ if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) {
82
+ dragMoved = true
83
+ isDragging = true
84
+ sidebar.isResizing = true
85
+ tooltipOpen = false
86
+ }
87
+ if (!dragMoved) return
88
+ if (sidebar.state === 'collapsed') {
89
+ if (delta > 0) {
90
+ endDrag()
91
+ sidebar.setOpen(true)
92
+ }
93
+ return
94
+ }
95
+ const targetWidth = dragStartWidthPx + delta
96
+ if (targetWidth < SIDEBAR_WIDTH_ICON_PX) {
97
+ endDrag()
98
+ sidebar.resetWidth()
99
+ sidebar.setOpen(false)
100
+ return
101
+ }
102
+ sidebar.setWidth(targetWidth)
103
+ }
104
+
105
+ function onWindowPointerUp(e: PointerEvent) {
106
+ if (e.pointerId !== activePointerId) return
107
+ endDrag()
72
108
  }
73
109
 
74
110
  function onPointerDown(e: PointerEvent) {
75
- if (sidebar.state !== 'expanded' || sidebar.isMobile) return
76
- const button = e.currentTarget as HTMLButtonElement
77
- const sidebarRoot = button.closest('[data-slot="sidebar"]')
111
+ if (sidebar.isMobile) return
112
+ if (e.button !== 0) return
113
+ const target = e.currentTarget as HTMLElement
114
+ const sidebarRoot = target.closest('[data-slot="sidebar"]')
78
115
  dragDirection = sidebarRoot?.getAttribute('data-side') === 'right' ? -1 : 1
79
116
 
80
117
  const container = sidebarRoot?.querySelector('[data-slot="sidebar-container"]')
81
118
  dragStartWidthPx = container instanceof HTMLElement ? container.offsetWidth : 256
82
119
  dragStartX = e.clientX
83
120
  dragMoved = false
84
- button.setPointerCapture(e.pointerId)
121
+ activePointerId = e.pointerId
85
122
  document.body.style.cursor = 'col-resize'
86
123
  document.body.style.userSelect = 'none'
124
+ window.addEventListener('pointermove', onWindowPointerMove)
125
+ window.addEventListener('pointerup', onWindowPointerUp)
126
+ window.addEventListener('pointercancel', onWindowPointerUp)
87
127
  }
88
128
 
89
- function onPointerMove(e: PointerEvent) {
90
- if (tooltipVisible) updateTooltipPosition(e)
91
- const button = e.currentTarget as HTMLButtonElement
92
- if (!button.hasPointerCapture(e.pointerId)) return
93
- const delta = (e.clientX - dragStartX) * dragDirection
94
- if (Math.abs(delta) > SIDEBAR_DRAG_THRESHOLD_PX) {
95
- dragMoved = true
96
- isDragging = true
97
- }
98
- if (dragMoved) sidebar.setWidth(dragStartWidthPx + delta)
99
- }
100
-
101
- function onPointerUp(e: PointerEvent) {
102
- const button = e.currentTarget as HTMLButtonElement
103
- if (button.hasPointerCapture(e.pointerId)) button.releasePointerCapture(e.pointerId)
104
- document.body.style.cursor = ''
105
- document.body.style.userSelect = ''
106
- isDragging = false
107
- }
129
+ const DOUBLE_CLICK_DELAY_MS = 150
130
+ let pendingClickTimer: ReturnType<typeof setTimeout> | undefined
108
131
 
109
132
  function onClick(e: MouseEvent) {
110
- if (dragMoved) {
133
+ if (Date.now() - dragEndTime < POST_DRAG_CLICK_GUARD_MS) {
111
134
  e.preventDefault()
112
135
  e.stopPropagation()
113
- dragMoved = false
114
136
  return
115
137
  }
116
- sidebar.toggle()
138
+ clearTimeout(pendingClickTimer)
139
+ pendingClickTimer = setTimeout(() => {
140
+ pendingClickTimer = undefined
141
+ sidebar.toggle()
142
+ }, DOUBLE_CLICK_DELAY_MS)
143
+ }
144
+
145
+ function onDoubleClick() {
146
+ if (sidebar.isMobile) return
147
+ clearTimeout(pendingClickTimer)
148
+ pendingClickTimer = undefined
149
+ sidebar.setOpen(true)
150
+ sidebar.resetWidth()
117
151
  }
118
152
  </script>
119
153
 
120
- <button
121
- bind:this={ref}
122
- data-sidebar="rail"
123
- data-slot="sidebar-rail"
124
- aria-label="Toggle Sidebar"
125
- tabindex={-1}
126
- type="button"
127
- onpointerenter={onPointerEnter}
128
- onpointerleave={onPointerLeave}
129
- onpointerdown={onPointerDown}
130
- onpointermove={onPointerMove}
131
- onpointerup={onPointerUp}
132
- onpointercancel={onPointerUp}
133
- onclick={onClick}
134
- class={cn(
135
- 'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
136
- 'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px]',
137
- 'hover:after:bg-sidebar-border',
138
- 'group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize',
139
- '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
140
- 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
141
- '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
142
- '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
143
- className
144
- )}
145
- {...restProps}
154
+ <TooltipPrimitive.Root
155
+ bind:open={tooltipOpen}
156
+ delayDuration={tooltipDelay}
157
+ disableHoverableContent
146
158
  >
147
- {@render children?.()}
148
- </button>
149
-
150
- {#if tooltipVisible}
151
- <div
152
- use:portal
153
- role="tooltip"
154
- bind:offsetWidth={tooltipWidth}
155
- bind:offsetHeight={tooltipHeight}
156
- class="fixed z-[1002] pointer-events-none rounded-md border border-border-inverse bg-background-default-negative px-3 py-2 text-sm font-medium text-foreground-inverse leading-5 tracking-tight shadow-md"
157
- style="left: {tooltipLeft}px; top: {tooltipTop}px; visibility: {tooltipWidth > 0
158
- ? 'visible'
159
- : 'hidden'};"
159
+ <TooltipPrimitive.Trigger>
160
+ {#snippet child({ props })}
161
+ {@const buttonProps = props as HTMLButtonAttributes}
162
+ <button
163
+ bind:this={ref}
164
+ {...buttonProps}
165
+ onpointerenter={(e) => {
166
+ buttonProps.onpointerenter?.(e)
167
+ onPointerEnter(e)
168
+ }}
169
+ onpointermove={(e) => {
170
+ buttonProps.onpointermove?.(e)
171
+ onPointerMoveOnTrigger(e)
172
+ }}
173
+ onpointerdown={(e) => {
174
+ buttonProps.onpointerdown?.(e)
175
+ onPointerDown(e)
176
+ }}
177
+ onclick={(e) => {
178
+ buttonProps.onclick?.(e)
179
+ onClick(e)
180
+ }}
181
+ ondblclick={onDoubleClick}
182
+ data-sidebar="rail"
183
+ data-slot="sidebar-rail"
184
+ aria-label="Toggle Sidebar"
185
+ tabindex={-1}
186
+ type="button"
187
+ class={cn(
188
+ 'absolute inset-y-0 z-50 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
189
+ 'after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:transition-colors after:duration-150',
190
+ 'hover:after:delay-150 hover:after:bg-background-accent-default',
191
+ 'group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize',
192
+ '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
193
+ 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
194
+ '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
195
+ '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
196
+ className
197
+ )}
198
+ {...restProps}
199
+ >
200
+ {@render children?.()}
201
+ </button>
202
+ {/snippet}
203
+ </TooltipPrimitive.Trigger>
204
+ <TooltipContent
205
+ customAnchor={cursorAnchor}
206
+ side="bottom"
207
+ align="center"
208
+ sideOffset={TOOLTIP_CURSOR_OFFSET}
160
209
  >
161
- <div class="flex flex-col gap-1.5">
210
+ <div class="flex flex-col gap-1">
162
211
  {#if sidebar.state === 'expanded'}
163
- <div>Drag to resize</div>
212
+ <div class="flex w-full items-center justify-between gap-3">
213
+ <span>Drag to resize</span>
214
+ <div class="flex items-center gap-0.5 opacity-0">
215
+ <ShortcutWrapper size="sm" theme="navigation">⌘</ShortcutWrapper>
216
+ <ShortcutWrapper size="sm" theme="navigation">.</ShortcutWrapper>
217
+ </div>
218
+ </div>
164
219
  {/if}
165
- <div class="flex items-center justify-between gap-3">
220
+ <div class="flex w-full items-center justify-between gap-3">
166
221
  <span>
167
222
  {sidebar.state === 'expanded' ? 'Click to collapse' : 'Click to expand'}
168
223
  </span>
169
- <div class="flex items-center gap-1">
170
- <ShortcutWrapper size="md" theme="navigation">⌘</ShortcutWrapper>
171
- <ShortcutWrapper size="md" theme="navigation">.</ShortcutWrapper>
224
+ <div class="flex items-center gap-0.5">
225
+ <ShortcutWrapper size="sm" theme="navigation">⌘</ShortcutWrapper>
226
+ <ShortcutWrapper size="sm" theme="navigation">.</ShortcutWrapper>
172
227
  </div>
173
228
  </div>
174
229
  </div>
175
- </div>
176
- {/if}
230
+ </TooltipContent>
231
+ </TooltipPrimitive.Root>
@@ -67,12 +67,13 @@
67
67
  data-collapsible={sidebar.state === 'collapsed' ? collapsible : ''}
68
68
  data-variant={variant}
69
69
  data-side={side}
70
+ data-resizing={sidebar.isResizing ? 'true' : undefined}
70
71
  data-slot="sidebar"
71
72
  >
72
73
  <div
73
74
  data-slot="sidebar-gap"
74
75
  class={cn(
75
- 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
76
+ 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-150 ease-linear group-data-[resizing=true]:transition-none',
76
77
  'group-data-[collapsible=offcanvas]:w-0',
77
78
  'group-data-[side=right]:rotate-180',
78
79
  variant === 'floating' || variant === 'inset'
@@ -83,7 +84,7 @@
83
84
  <div
84
85
  data-slot="sidebar-container"
85
86
  class={cn(
86
- 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
87
+ 'fixed inset-y-0 z-50 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-150 ease-linear group-data-[resizing=true]:transition-none md:flex',
87
88
  side === 'left'
88
89
  ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
89
90
  : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
@@ -8,7 +8,8 @@
8
8
  side = 'top',
9
9
  children,
10
10
  showArrow = false,
11
- arrowClasses
11
+ arrowClasses,
12
+ ...rest
12
13
  }: TooltipPrimitive.ContentProps & {
13
14
  showArrow?: boolean
14
15
  arrowClasses?: string
@@ -21,8 +22,9 @@
21
22
  data-slot="tooltip-content"
22
23
  {sideOffset}
23
24
  {side}
25
+ {...rest}
24
26
  class={cn(
25
- 'bg-background-default-negative border border-border-inverse z-[1002] rounded-md px-2 py-1 text-sm font-medium text-foreground-inverse leading-5 tracking-tight shadow-md',
27
+ 'bg-background-default-negative border border-border-inverse z-1002 rounded-md px-2 py-1 text-base font-medium text-foreground-inverse shadow-md',
26
28
  className
27
29
  )}
28
30
  >
@@ -32,11 +34,11 @@
32
34
  {#snippet child({ props })}
33
35
  <div
34
36
  class={cn(
35
- 'bg-background-default-negative z-[1002] size-2.5 rotate-45 rounded-[2px]',
36
- 'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]',
37
- 'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]',
38
- 'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2',
39
- 'data-[side=left]:-translate-y-[calc(50%_-_3px)]',
37
+ 'bg-background-default-negative z-1002 size-2.5 rotate-45 rounded-xs',
38
+ 'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%+2px)]',
39
+ 'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%+1px)]',
40
+ 'data-[side=right]:translate-x-[calc(50%+2px)] data-[side=right]:translate-y-1/2',
41
+ 'data-[side=left]:-translate-y-[calc(50%-3px)]',
40
42
  arrowClasses
41
43
  )}
42
44
  {...props}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@invopop/popui",
3
3
  "license": "MIT",
4
- "version": "0.1.99",
4
+ "version": "0.1.101",
5
5
  "repository": {
6
6
  "url": "https://github.com/invopop/popui"
7
7
  },