@hyvor/design 1.1.25-beta-org-19 → 2.0.0-beta.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.
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ pictureUrl: string | null;
4
+ name: string;
5
+ size?: number;
6
+ }
7
+
8
+ let { pictureUrl, name, size = 30 }: Props = $props();
9
+ </script>
10
+
11
+ {#if pictureUrl}
12
+ <img src={pictureUrl} alt={name} style="width: {size}px; height: {size}px;" />
13
+ {:else}
14
+ <span class="user-placeholder" style="width: {size}px; height: {size}px;">
15
+ {name ? name[0].toUpperCase() : '?'}
16
+ </span>
17
+ {/if}
18
+
19
+ <style>
20
+ img {
21
+ border-radius: 50%;
22
+ }
23
+ .user-placeholder {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ border-radius: 50%;
28
+ color: var(--text);
29
+ font-size: 14px;
30
+ background-color: var(--input);
31
+ }
32
+ </style>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ pictureUrl: string | null;
3
+ name: string;
4
+ size?: number;
5
+ }
6
+ declare const UserPicture: import("svelte").Component<Props, {}, "">;
7
+ type UserPicture = ReturnType<typeof UserPicture>;
8
+ export default UserPicture;
@@ -57,7 +57,7 @@ export interface CloudContext {
57
57
  export interface CloudContextUser {
58
58
  id: number;
59
59
  name: string | null;
60
- username?: string | null;
60
+ username?: string;
61
61
  email: string;
62
62
  picture_url: string | null;
63
63
  }
@@ -81,5 +81,6 @@ export interface OrganizationMember {
81
81
  user_id: number;
82
82
  user_username: string;
83
83
  user_email: string;
84
+ user_picture_url: string | null;
84
85
  }
85
86
  export type OrganizationRole = 'admin' | 'manager' | 'member' | 'billing';
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
+ import UserPicture from '../@components/UserPicture.svelte';
2
3
  import { getCloudContext } from '../CloudContext/cloudContext.svelte.js';
3
4
 
4
5
  const { user } = $derived(getCloudContext());
5
6
  </script>
6
7
 
8
+ <UserPicture pictureUrl={user.picture_url} name={user.username || user.name || ''} size={30} />
9
+
7
10
  {#if user.picture_url}
8
11
  <img src={user.picture_url} alt={user.name} />
9
12
  {:else}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { toast } from '../../components/index.js';
2
+ import { clickOutside, IconMessage, toast } from '../../components/index.js';
3
3
  import Loader from '../../components/Loader/Loader.svelte';
4
4
  import TextInput from '../../components/TextInput/TextInput.svelte';
5
5
  import {
@@ -7,18 +7,30 @@
7
7
  type OrganizationMember
8
8
  } from '../CloudContext/cloudContext.svelte.js';
9
9
  import { searchMembers } from './members.js';
10
+ import { dropdownSlide } from '../../components/Dropdown/dropdownSlide.js';
11
+ import UserPicture from '../@components/UserPicture.svelte';
12
+ import IconButton from '../../components/IconButton/IconButton.svelte';
13
+
14
+ interface Props {
15
+ selectedUserId?: number;
16
+ }
17
+ let { selectedUserId = $bindable(undefined) }: Props = $props();
10
18
 
11
19
  const { instance, organization } = getCloudContext();
12
20
 
13
21
  let search = $state('');
14
22
  let role = $derived(organization?.role);
15
- let focused = $state(false);
23
+ let showable = $state(false);
16
24
  let loading = $state(false);
17
25
  let members: undefined | OrganizationMember[] = $state(undefined);
18
26
 
27
+ let selectedMember = $state(null as null | OrganizationMember);
28
+
19
29
  let searchTimeout: ReturnType<typeof setTimeout> | null = null;
20
30
 
21
31
  function handleKeyUp() {
32
+ showable = true;
33
+
22
34
  if (searchTimeout) {
23
35
  clearTimeout(searchTimeout);
24
36
  }
@@ -44,43 +56,99 @@
44
56
  });
45
57
  }, 500);
46
58
  }
59
+
60
+ function handleSelect(member: OrganizationMember) {
61
+ selectedMember = member;
62
+ selectedUserId = member.user_id;
63
+ showable = false;
64
+ }
65
+
66
+ function handleDeselect() {
67
+ selectedMember = null;
68
+ selectedUserId = undefined;
69
+ }
47
70
  </script>
48
71
 
49
- <div class="search-wrap">
50
- <TextInput
51
- placeholder="Search member by email..."
52
- bind:value={search}
53
- block
54
- onkeyup={handleKeyUp}
55
- onfocus={() => (focused = true)}
56
- onblur={() => (focused = false)}
57
- >
58
- {#snippet end()}
59
- {#if loading}
60
- <Loader size={12} colorTrack="transparent" />
61
- {/if}
62
- {/snippet}
63
- </TextInput>
64
-
65
- {#if members !== undefined && focused}
66
- <div class="search-results hds-box">
67
- {#each members as member}
68
- {member.user_email}
69
- {/each}
72
+ {#if selectedMember}
73
+ <div class="selected-wrap">
74
+ <div class="selected-user">
75
+ <UserPicture
76
+ pictureUrl={selectedMember.user_picture_url}
77
+ name={selectedMember.user_username || selectedMember.user_email}
78
+ size={30}
79
+ />
80
+
81
+ <div class="user-data">
82
+ <div class="username">
83
+ @{selectedMember.user_username}
84
+ </div>
85
+ <div class="email">{selectedMember.user_email}</div>
86
+ </div>
87
+ </div>
88
+ <div>
89
+ <IconButton size="small" color="input" onclick={handleDeselect}>&times;</IconButton>
70
90
  </div>
71
- {/if}
72
- </div>
73
-
74
- <div class="invite-note">
75
- Looking for a user outside your organization?
76
- {#if role === 'admin' || role === 'manager'}
77
- <a href={instance + '/account/org/members?invite'} target="_blank" class="hds-link">
78
- Invite them
79
- </a>
80
- {:else}
81
- Ask an admin to invite them
82
- {/if} to your organization first.
83
- </div>
91
+ </div>
92
+ {:else}
93
+ <div
94
+ class="search-wrap"
95
+ use:clickOutside={{
96
+ callback: () => (showable = false)
97
+ }}
98
+ >
99
+ <TextInput
100
+ placeholder="Search member by email..."
101
+ bind:value={search}
102
+ block
103
+ onkeyup={handleKeyUp}
104
+ >
105
+ {#snippet end()}
106
+ {#if loading}
107
+ <Loader size={12} colorTrack="transparent" />
108
+ {/if}
109
+ {/snippet}
110
+ </TextInput>
111
+
112
+ {#if members !== undefined && showable}
113
+ <div class="search-results hds-box" transition:dropdownSlide>
114
+ {#if members.length}
115
+ {#each members as member}
116
+ <button class="member-row" onclick={() => handleSelect(member)}>
117
+ <UserPicture
118
+ pictureUrl={member.user_picture_url}
119
+ name={member.user_username || member.user_email}
120
+ size={30}
121
+ />
122
+
123
+ <div class="user-data">
124
+ <div class="username">
125
+ @{member.user_username}
126
+ </div>
127
+ <div class="email">{member.user_email}</div>
128
+ </div>
129
+
130
+ <div class="role">
131
+ {member.role}
132
+ </div>
133
+ </button>
134
+ {/each}
135
+ {:else}
136
+ <IconMessage empty padding={30} iconSize={35} message="No members found" />
137
+ {/if}
138
+ </div>
139
+ {/if}
140
+ </div>
141
+ <div class="invite-note">
142
+ Looking for a user outside your organization?
143
+ {#if role === 'admin' || role === 'manager'}
144
+ <a href={instance + '/account/org/members?invite'} target="_blank" class="hds-link">
145
+ Invite them
146
+ </a>
147
+ {:else}
148
+ Ask an admin to invite them
149
+ {/if} to your organization first.
150
+ </div>
151
+ {/if}
84
152
 
85
153
  <style>
86
154
  .invite-note {
@@ -95,7 +163,56 @@
95
163
  position: absolute;
96
164
  top: 100%;
97
165
  width: 100%;
98
- padding: 15px;
99
166
  margin-top: 10px;
167
+ overflow: auto;
168
+ z-index: 10;
169
+ max-height: 300px;
170
+ }
171
+
172
+ .member-row {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 10px;
176
+ padding: 15px 25px;
177
+ transition: 0.1s background-color;
178
+ cursor: pointer;
179
+ text-align: left;
180
+ width: 100%;
181
+ border-radius: 20px;
182
+ }
183
+
184
+ .member-row:hover {
185
+ background-color: var(--hover);
186
+ }
187
+
188
+ .username {
189
+ font-weight: 600;
190
+ }
191
+ .email {
192
+ color: var(--text-light);
193
+ font-size: 14px;
194
+ }
195
+
196
+ .user-data {
197
+ flex: 1;
198
+ }
199
+
200
+ .role {
201
+ text-transform: uppercase;
202
+ color: var(--text-light);
203
+ font-size: 13px;
204
+ font-weight: 600;
205
+ }
206
+
207
+ .selected-wrap {
208
+ display: flex;
209
+ align-items: center;
210
+ }
211
+
212
+ .selected-user {
213
+ display: flex;
214
+ flex: 1;
215
+ align-items: center;
216
+ gap: 6px;
100
217
  }
101
218
  </style>
@@ -1,3 +1,6 @@
1
- declare const OrganizationMembersSearch: import("svelte").Component<Record<string, never>, {}, "">;
1
+ interface Props {
2
+ selectedUserId?: number;
3
+ }
4
+ declare const OrganizationMembersSearch: import("svelte").Component<Props, {}, "selectedUserId">;
2
5
  type OrganizationMembersSearch = ReturnType<typeof OrganizationMembersSearch>;
3
6
  export default OrganizationMembersSearch;
@@ -3,3 +3,4 @@ export { getCloudContext, setCloudContext, type CloudContext as CloudContextType
3
3
  export { default as HyvorBar } from './HyvorBar/HyvorBar.svelte';
4
4
  export { bar as hyvorBar } from './HyvorBar/bar.js';
5
5
  export { default as ResourceCreator } from './ResourceCreator/ResourceCreator.svelte';
6
+ export { default as OrganizationMemberSearch } from './OrganizationMembers/OrganizationMembersSearch.svelte';
@@ -3,3 +3,4 @@ export { getCloudContext, setCloudContext } from './CloudContext/cloudContext.sv
3
3
  export { default as HyvorBar } from './HyvorBar/HyvorBar.svelte';
4
4
  export { bar as hyvorBar } from './HyvorBar/bar.js';
5
5
  export { default as ResourceCreator } from './ResourceCreator/ResourceCreator.svelte';
6
+ export { default as OrganizationMemberSearch } from './OrganizationMembers/OrganizationMembersSearch.svelte';
@@ -2,8 +2,8 @@
2
2
  import { onMount } from 'svelte';
3
3
  import { clickOutside } from '../directives/clickOutside.js';
4
4
  import debounce from '../directives/debounce.js';
5
- import { elasticInOut } from 'svelte/easing';
6
5
  import type { DropdownAlign, DropdownPosition } from './dropdown.types.js';
6
+ import { dropdownSlide } from './dropdownSlide.js';
7
7
 
8
8
  interface Props {
9
9
  show: boolean;
@@ -116,19 +116,6 @@
116
116
  mutationObserver.disconnect();
117
117
  };
118
118
  });
119
-
120
- function slideIn(node: any) {
121
- return {
122
- duration: 100,
123
- easing: elasticInOut,
124
- css: (t: number) => {
125
- return `
126
- opacity: ${0.2 + t * 0.8};
127
- transform: translateY(-${(1 - t) * 5}px) scale(${0.95 + t * 0.05});
128
- `;
129
- }
130
- };
131
- }
132
119
  </script>
133
120
 
134
121
  <svelte:window onresize={debouncedPosition} onscroll={debouncedPosition} />
@@ -141,7 +128,7 @@
141
128
  }}
142
129
  bind:this={contentWrap}
143
130
  style="width: {width}px"
144
- transition:slideIn
131
+ transition:dropdownSlide
145
132
  >
146
133
  <div class="hds-box content" style:padding="{padding}px">
147
134
  {@render children?.()}
@@ -0,0 +1,6 @@
1
+ import { elasticInOut } from 'svelte/easing';
2
+ export declare function dropdownSlide(node: any): {
3
+ duration: number;
4
+ easing: typeof elasticInOut;
5
+ css: (t: number) => string;
6
+ };
@@ -0,0 +1,13 @@
1
+ import { elasticInOut } from 'svelte/easing';
2
+ export function dropdownSlide(node) {
3
+ return {
4
+ duration: 100,
5
+ easing: elasticInOut,
6
+ css: (t) => {
7
+ return `
8
+ opacity: ${0.2 + t * 0.8};
9
+ transform: translateY(-${(1 - t) * 5}px) scale(${0.95 + t * 0.05});
10
+ `;
11
+ }
12
+ };
13
+ }
package/package.json CHANGED
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "version": "1.1.25-beta-org-19"
73
+ "version": "2.0.0-beta.1"
74
74
  }