@stubber/ui 1.6.1 → 1.6.2

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.
@@ -1,8 +1,12 @@
1
1
  <script>
2
- import { createEventDispatcher } from "svelte";
2
+ import { createEventDispatcher, tick } from "svelte";
3
3
  import { slide } from "svelte/transition";
4
4
  import { quintOut } from "svelte/easing";
5
- import { clickOutside } from "../utils/actions.js";
5
+ import * as Popover from "@stubber/ui/popover";
6
+ import * as Command from "@stubber/ui/command";
7
+ import * as Tooltip from "@stubber/ui/tooltip";
8
+ import { Button } from "@stubber/ui/button";
9
+ import { toast } from "svelte-sonner";
6
10
 
7
11
  export let orgs = [];
8
12
  export let selectedorguuid = undefined;
@@ -18,66 +22,76 @@
18
22
  success: "success-500",
19
23
  };
20
24
 
21
- let filterText = "";
25
+ // Combobox state
26
+ let open = false;
27
+ let searchValue = "";
22
28
 
23
- //state
24
- let state = {
25
- isMenu: false,
26
- };
27
-
28
- function toggleMenu() {
29
- if (orgs.length > 0) {
30
- state.isMenu = !state.isMenu;
31
- }
32
- }
29
+ // Filter orgs based on search
30
+ $: filteredOrgs = orgs.filter((org) =>
31
+ org.label.toLowerCase().includes(searchValue.toLowerCase())
32
+ );
33
33
 
34
34
  function selectOrg(orguuid) {
35
35
  dispatch("selectOrg", orguuid);
36
- selectedorguuid = orguuid;
37
- state.isMenu = false;
38
36
  }
39
37
 
40
- function closeMenu() {
41
- if (state.isMenu) {
42
- filterText = "";
43
- state.isMenu = false;
38
+ function closeAndFocusTrigger(triggerId) {
39
+ open = false;
40
+ searchValue = "";
41
+ tick().then(() => {
42
+ document.getElementById(triggerId)?.focus();
43
+ });
44
+ }
45
+
46
+ function copyToClipboard(uuid, event) {
47
+ if (event) {
48
+ event.stopPropagation();
49
+ event.preventDefault();
44
50
  }
51
+ navigator.clipboard.writeText(uuid);
52
+ toast.success("UUID copied to clipboard!");
45
53
  }
46
54
  </script>
47
55
 
48
- <div class="relative">
49
- <div use:clickOutside={closeMenu} class="{state.isMenu ? 'absolute top-0' : ''} bg-white w-fit">
50
- {#if (orgs.length >= 10 && !state.isMenu) || orgs.length <= 10}
51
- <button
52
- on:click|stopPropagation={toggleMenu}
53
- class="flex items-center {state.isMenu
54
- ? 'w-[230px] sm:w-[280px] border-x border-t rounded-x-lg rounded-t-lg hover:bg-primary-50'
55
- : 'sm:w-[280px] border rounded-lg'} border-surface-100"
56
- >
57
- <div class="h-10 flex items-center justify-center aspect-square">
56
+ <Popover.Root bind:open let:ids>
57
+ <Popover.Trigger asChild let:builder>
58
+ <Button
59
+ builders={[builder]}
60
+ id={ids.trigger}
61
+ variant="outline"
62
+ role="combobox"
63
+ aria-expanded={open}
64
+ class="w-full sm:w-[280px] pl-3 pr-3 py-2 h-auto border-surface-100 transition-colors group justify-start"
65
+ >
66
+ <div class="flex items-center w-full">
67
+ <div class="flex items-center justify-center mr-3">
58
68
  {#if selectedOrg?.filesrc && !loading}
59
69
  <img
60
- class="h-[25px] aspect-square object-cover rounded-full"
70
+ class="h-[25px] w-[25px] object-cover rounded-full"
61
71
  src={selectedOrg.filesrc}
62
72
  alt={selectedOrg.label}
63
73
  />
74
+ {:else if selectedOrg?.placeholderColor}
75
+ <div
76
+ class="h-[25px] w-[25px] rounded-full"
77
+ style="background-color: {selectedOrg.placeholderColor}"
78
+ />
64
79
  {:else}
65
- <div class="h-[25px] aspect-square rounded-full bg-surface-100" />
80
+ <div class="h-[25px] w-[25px] rounded-full bg-surface-100" />
66
81
  {/if}
67
82
  </div>
83
+
68
84
  {#if loading}
69
- <span class="hidden sm:block h-[25px] w-40 bg-surface-100 rounded-md" />
85
+ <span class="h-[25px] w-40 bg-surface-100 rounded-md" />
70
86
  {:else}
71
- <div class="{state.isMenu ? '' : 'hidden'} sm:block w-full text-left truncate">
87
+ <div class="w-full text-left truncate">
72
88
  <p class="text-label text-surface-900 truncate">
73
- {selectedOrg?.label ?? "No orgs"}
89
+ {selectedOrg?.label ?? "No Org"}
74
90
  </p>
75
91
  {#if selectedOrg?.alert}
76
92
  <div class="flex items-center space-x-1">
77
93
  <span
78
- class="h-[6px] aspect-square bg-{alertColorMap[
79
- selectedOrg.alert?.type
80
- ]} rounded-full"
94
+ class="h-[6px] w-[6px] bg-{alertColorMap[selectedOrg.alert?.type]} rounded-full"
81
95
  />
82
96
  <p class="text-small italic text-{alertColorMap[selectedOrg.alert?.type]}">
83
97
  {selectedOrg.alert?.message}
@@ -86,71 +100,82 @@
86
100
  {/if}
87
101
  </div>
88
102
  {/if}
89
- {#if !state.isMenu && selectedOrg?.alert}
90
- <span
91
- class="block sm:hidden h-[6px] aspect-square bg-{alertColorMap[
92
- selectedOrg.alert?.type
93
- ]} rounded-full"
94
- />
103
+
104
+ {#if selectedOrg}
105
+ <Tooltip.Root openDelay={0} disableHoverableContent={true}>
106
+ <Tooltip.Trigger>
107
+ <div
108
+ class="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity ml-2 mr-2"
109
+ >
110
+ <i
111
+ role="button"
112
+ tabindex="0"
113
+ on:click={(e) => {
114
+ e.stopPropagation();
115
+ copyToClipboard(selectedOrg.orguuid, e);
116
+ }}
117
+ on:keydown={(e) => {
118
+ if (e.key === "Enter" || e.key === " ") {
119
+ e.stopPropagation();
120
+ copyToClipboard(selectedOrg.orguuid, e);
121
+ }
122
+ }}
123
+ class="far fa-copy text-surface-400 cursor-pointer"
124
+ />
125
+ </div>
126
+ </Tooltip.Trigger>
127
+ <Tooltip.Content>Copy UUID</Tooltip.Content>
128
+ </Tooltip.Root>
95
129
  {/if}
96
- <div
97
- class="{loading
98
- ? 'block sm:hidden'
99
- : ''} ml-auto h-10 aspect-square flex items-center justify-center"
100
- >
101
- <i class="text-surface-900 fa far fa-chevron-down {state.isMenu ? 'rotate down' : ''}" />
102
- </div>
103
- </button>
104
- {:else}
105
- <div
106
- class="h-10 w-[230px] sm:w-[280px] flex items-center border-x border-t rounded-x-lg rounded-t-lg"
107
- >
108
- <div class="h-full aspect-square flex items-center justify-center">
109
- <i class="text-surface-900 fa far fa-magnifying-glass" />
110
- </div>
111
- <input
112
- type="text"
113
- autofocus
114
- class="w-full pr-3 text-surface-900 text-label focus:outline-none"
115
- placeholder="Search orgs"
116
- bind:value={filterText}
117
- />
118
130
  </div>
119
- {/if}
131
+ </Button>
132
+ </Popover.Trigger>
120
133
 
121
- {#if state.isMenu}
122
- <div
123
- in:slide={{ duration: 300, easing: quintOut, axis: "y" }}
124
- class="w-[230px] sm:w-[280px] border-x border-b rounded-x-lg rounded-b-lg"
125
- >
126
- <div class="max-h-80 overflow-y-auto">
127
- {#each orgs.filter((o) => o.orguuid !== selectedOrg.orguuid || orgs.length >= 10) as org}
128
- {#if org.label.toLowerCase().includes(filterText.toLowerCase())}
129
- <button
130
- on:click={() => selectOrg(org.orguuid)}
131
- class="w-full flex items-center hover:bg-primary-50 focus:bg-primary-50 focus:outline-none"
132
- >
133
- <div class="h-10 flex items-center justify-center aspect-square">
134
+ <Popover.Content
135
+ class="w-full sm:w-[280px] p-0"
136
+ transition={slide}
137
+ transitionConfig={{ duration: 300, easing: quintOut, axis: "y" }}
138
+ >
139
+ <Command.Root shouldFilter={false}>
140
+ <Command.Input placeholder="Search organizations..." bind:value={searchValue} />
141
+ <Command.Empty>No organizations found.</Command.Empty>
142
+ <Command.Group class="max-h-80 overflow-y-auto">
143
+ {#each filteredOrgs as org}
144
+ <div class="relative group">
145
+ <Command.Item
146
+ value={org.orguuid}
147
+ onSelect={() => {
148
+ selectOrg(org.orguuid);
149
+ closeAndFocusTrigger(ids.trigger);
150
+ }}
151
+ class="pr-12"
152
+ >
153
+ <div class="flex items-center w-full">
154
+ <div class="flex items-center justify-center mr-3">
134
155
  {#if org.filesrc}
135
156
  <img
136
- class="h-[25px] aspect-square object-cover rounded-full"
157
+ class="h-[25px] w-[25px] object-cover rounded-full"
137
158
  src={org.filesrc}
138
159
  alt={org.label}
139
160
  />
161
+ {:else if org.placeholderColor}
162
+ <div
163
+ class="h-[25px] w-[25px] rounded-full"
164
+ style="background-color: {org.placeholderColor}"
165
+ />
140
166
  {:else}
141
- <div class="h-[25px] aspect-square rounded-full bg-surface-100" />
167
+ <div class="h-[25px] w-[25px] rounded-full bg-surface-100" />
142
168
  {/if}
143
169
  </div>
144
- <div class="{state.isMenu ? '' : 'hidden'} sm:block w-full text-left truncate">
145
- <p class="text-label text-surface-900 truncate">
170
+
171
+ <div class="flex-1 text-left">
172
+ <p class="text-label text-surface-900">
146
173
  {org.label}
147
174
  </p>
148
175
  {#if org.alert}
149
176
  <div class="flex items-center space-x-1">
150
177
  <span
151
- class="h-[6px] aspect-square bg-{alertColorMap[
152
- org.alert.type
153
- ]} rounded-full"
178
+ class="h-[6px] w-[6px] bg-{alertColorMap[org.alert.type]} rounded-full"
154
179
  />
155
180
  <p class="text-small italic text-{alertColorMap[org.alert.type]}">
156
181
  {org.alert?.message}
@@ -158,36 +183,37 @@
158
183
  </div>
159
184
  {/if}
160
185
  </div>
161
- {#if org.orguuid === selectedOrg.orguuid}
162
- <div class="ml-auto h-10 aspect-square flex items-center justify-center">
163
- <i class="text-success-500 fa far fa-check" />
164
- </div>
165
- {/if}
166
- </button>
167
- {/if}
168
- {/each}
169
- </div>
170
-
171
- {#if createOrgLink}
172
- <a
173
- href={createOrgLink}
174
- class="h-10 w-full pl-3.5 pr-3 flex items-center bg-white text-primary-500 rounded-b-lg hover:bg-primary-50 focus:bg-primary-50 focus:outline-none hover:text-primary-400"
175
- >
176
- <i class="far fa fa-plus mr-2" />
177
- <span class="text-placeholder">Create org</span>
178
- </a>
179
- {/if}
180
- </div>
181
- {/if}
182
- </div>
183
- </div>
186
+ </div>
187
+ </Command.Item>
184
188
 
185
- <style>
186
- .rotate {
187
- transition: all 150ms linear;
188
- }
189
+ <!-- Copy button positioned absolutely -->
190
+ <div class="absolute right-2 top-1/2 -translate-y-1/2">
191
+ <Button
192
+ variant="ghost"
193
+ size="icon"
194
+ class="h-8 w-8 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
195
+ on:click={(e) => {
196
+ e.stopPropagation();
197
+ copyToClipboard(org.orguuid, e);
198
+ }}
199
+ >
200
+ <i class="far fa-copy text-surface-400" />
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ {/each}
205
+ </Command.Group>
189
206
 
190
- .rotate.down {
191
- transform: rotate(180deg);
192
- }
193
- </style>
207
+ {#if createOrgLink}
208
+ <Command.Separator />
209
+ <a
210
+ href={createOrgLink}
211
+ class="flex items-center px-8 py-2 text-sm text-primary-500 hover:bg-accent rounded-sm"
212
+ >
213
+ <i class="far fa-plus mr-2" />
214
+ <span>Create org</span>
215
+ </a>
216
+ {/if}
217
+ </Command.Root>
218
+ </Popover.Content>
219
+ </Popover.Root>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubber/ui",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run package && node ./fix_dts_imports.js",